Editor2

Conrad Irwin created

Change summary

crates/editor2/Cargo.toml                          |    92 
crates/editor2/src/blink_manager.rs                |   113 
crates/editor2/src/display_map.rs                  |  1912 +++
crates/editor2/src/display_map/block_map.rs        |  1667 ++
crates/editor2/src/display_map/fold_map.rs         |  1706 ++
crates/editor2/src/display_map/inlay_map.rs        |  1895 +++
crates/editor2/src/display_map/tab_map.rs          |   765 +
crates/editor2/src/display_map/wrap_map.rs         |  1355 ++
crates/editor2/src/editor.rs                       | 10095 ++++++++++++++++
crates/editor2/src/editor_settings.rs              |    62 
crates/editor2/src/editor_tests.rs                 |  8195 ++++++++++++
crates/editor2/src/element.rs                      |  3478 +++++
crates/editor2/src/git.rs                          |   282 
crates/editor2/src/highlight_matching_bracket.rs   |   138 
crates/editor2/src/hover_popover.rs                |  1329 ++
crates/editor2/src/inlay_hint_cache.rs             |  3349 +++++
crates/editor2/src/items.rs                        |  1327 ++
crates/editor2/src/link_go_to_definition.rs        |  1269 ++
crates/editor2/src/mouse_context_menu.rs           |    96 
crates/editor2/src/movement.rs                     |   927 +
crates/editor2/src/persistence.rs                  |    83 
crates/editor2/src/scroll.rs                       |   436 
crates/editor2/src/scroll/actions.rs               |   152 
crates/editor2/src/scroll/autoscroll.rs            |   258 
crates/editor2/src/scroll/scroll_amount.rs         |    28 
crates/editor2/src/selections_collection.rs        |   886 +
crates/editor2/src/test.rs                         |    83 
crates/editor2/src/test/editor_lsp_test_context.rs |   297 
crates/editor2/src/test/editor_test_context.rs     |   332 
29 files changed, 42,607 insertions(+)

Detailed changes

crates/editor2/Cargo.toml ๐Ÿ”—

@@ -0,0 +1,92 @@
+[package]
+name = "editor"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/editor.rs"
+doctest = false
+
+[features]
+test-support = [
+    "copilot/test-support",
+    "text/test-support",
+    "language/test-support",
+    "gpui/test-support",
+    "multi_buffer/test-support",
+    "project/test-support",
+    "util/test-support",
+    "workspace/test-support",
+    "tree-sitter-rust",
+    "tree-sitter-typescript"
+]
+
+[dependencies]
+client = { path = "../client" }
+clock = { path = "../clock" }
+copilot = { path = "../copilot" }
+db = { path = "../db" }
+drag_and_drop = { path = "../drag_and_drop" }
+collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
+fuzzy = { path = "../fuzzy" }
+git = { path = "../git" }
+gpui = { path = "../gpui" }
+language = { path = "../language" }
+lsp = { path = "../lsp" }
+multi_buffer = { path = "../multi_buffer" }
+project = { path = "../project" }
+rpc = { path = "../rpc" }
+rich_text = { path = "../rich_text" }
+settings = { path = "../settings" }
+snippet = { path = "../snippet" }
+sum_tree = { path = "../sum_tree" }
+text = { path = "../text" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+sqlez = { path = "../sqlez" }
+workspace = { path = "../workspace" }
+
+aho-corasick = "1.1"
+anyhow.workspace = true
+convert_case = "0.6.0"
+futures.workspace = true
+indoc = "1.0.4"
+itertools = "0.10"
+lazy_static.workspace = true
+log.workspace = true
+ordered-float.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+rand.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smallvec.workspace = true
+smol.workspace = true
+
+tree-sitter-rust = { workspace = true, optional = true }
+tree-sitter-html = { workspace = true, optional = true }
+tree-sitter-typescript = { workspace = true, optional = true }
+
+[dev-dependencies]
+copilot = { path = "../copilot", features = ["test-support"] }
+text = { path = "../text", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
+lsp = { path = "../lsp", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
+multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
+
+ctor.workspace = true
+env_logger.workspace = true
+rand.workspace = true
+unindent.workspace = true
+tree-sitter.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-html.workspace = true
+tree-sitter-typescript.workspace = true
@@ -0,0 +1,113 @@
+use crate::EditorSettings;
+use gpui::{Entity, ModelContext};
+use settings::SettingsStore;
+use smol::Timer;
+use std::time::Duration;
+
+pub struct BlinkManager {
+    blink_interval: Duration,
+
+    blink_epoch: usize,
+    blinking_paused: bool,
+    visible: bool,
+    enabled: bool,
+}
+
+impl BlinkManager {
+    pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
+        // Make sure we blink the cursors if the setting is re-enabled
+        cx.observe_global::<SettingsStore, _>(move |this, cx| {
+            this.blink_cursors(this.blink_epoch, cx)
+        })
+        .detach();
+
+        Self {
+            blink_interval,
+
+            blink_epoch: 0,
+            blinking_paused: false,
+            visible: true,
+            enabled: false,
+        }
+    }
+
+    fn next_blink_epoch(&mut self) -> usize {
+        self.blink_epoch += 1;
+        self.blink_epoch
+    }
+
+    pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
+        self.show_cursor(cx);
+
+        let epoch = self.next_blink_epoch();
+        let interval = self.blink_interval;
+        cx.spawn(|this, mut cx| {
+            let this = this.downgrade();
+            async move {
+                Timer::after(interval).await;
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
+                }
+            }
+        })
+        .detach();
+    }
+
+    fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
+        if epoch == self.blink_epoch {
+            self.blinking_paused = false;
+            self.blink_cursors(epoch, cx);
+        }
+    }
+
+    fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
+        if settings::get::<EditorSettings>(cx).cursor_blink {
+            if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
+                self.visible = !self.visible;
+                cx.notify();
+
+                let epoch = self.next_blink_epoch();
+                let interval = self.blink_interval;
+                cx.spawn(|this, mut cx| {
+                    let this = this.downgrade();
+                    async move {
+                        Timer::after(interval).await;
+                        if let Some(this) = this.upgrade(&cx) {
+                            this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
+                        }
+                    }
+                })
+                .detach();
+            }
+        } else {
+            self.show_cursor(cx);
+        }
+    }
+
+    pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) {
+        if !self.visible {
+            self.visible = true;
+            cx.notify();
+        }
+    }
+
+    pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
+        self.enabled = true;
+        // Set cursors as invisible and start blinking: this causes cursors
+        // to be visible during the next render.
+        self.visible = false;
+        self.blink_cursors(self.blink_epoch, cx);
+    }
+
+    pub fn disable(&mut self, _cx: &mut ModelContext<Self>) {
+        self.enabled = false;
+    }
+
+    pub fn visible(&self) -> bool {
+        self.visible
+    }
+}
+
+impl Entity for BlinkManager {
+    type Event = ();
+}

crates/editor2/src/display_map.rs ๐Ÿ”—

@@ -0,0 +1,1912 @@
+mod block_map;
+mod fold_map;
+mod inlay_map;
+mod tab_map;
+mod wrap_map;
+
+use crate::{
+    link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
+    EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+};
+pub use block_map::{BlockMap, BlockPoint};
+use collections::{BTreeMap, HashMap, HashSet};
+use fold_map::FoldMap;
+use gpui::{
+    color::Color,
+    fonts::{FontId, HighlightStyle, Underline},
+    text_layout::{Line, RunStyle},
+    Entity, ModelContext, ModelHandle,
+};
+use inlay_map::InlayMap;
+use language::{
+    language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
+};
+use lsp::DiagnosticSeverity;
+use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
+use sum_tree::{Bias, TreeMap};
+use tab_map::TabMap;
+use wrap_map::WrapMap;
+
+pub use block_map::{
+    BlockBufferRows as DisplayBufferRows, BlockChunks as DisplayChunks, BlockContext,
+    BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
+};
+
+pub use self::fold_map::FoldPoint;
+pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum FoldStatus {
+    Folded,
+    Foldable,
+}
+
+pub trait ToDisplayPoint {
+    fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
+}
+
+type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
+type InlayHighlights = BTreeMap<TypeId, HashMap<InlayId, (HighlightStyle, InlayHighlight)>>;
+
+pub struct DisplayMap {
+    buffer: ModelHandle<MultiBuffer>,
+    buffer_subscription: BufferSubscription,
+    fold_map: FoldMap,
+    inlay_map: InlayMap,
+    tab_map: TabMap,
+    wrap_map: ModelHandle<WrapMap>,
+    block_map: BlockMap,
+    text_highlights: TextHighlights,
+    inlay_highlights: InlayHighlights,
+    pub clip_at_line_ends: bool,
+}
+
+impl Entity for DisplayMap {
+    type Event = ();
+}
+
+impl DisplayMap {
+    pub fn new(
+        buffer: ModelHandle<MultiBuffer>,
+        font_id: FontId,
+        font_size: f32,
+        wrap_width: Option<f32>,
+        buffer_header_height: u8,
+        excerpt_header_height: u8,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+
+        let tab_size = Self::tab_size(&buffer, cx);
+        let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+        let (fold_map, snapshot) = FoldMap::new(snapshot);
+        let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
+        let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
+        let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
+        cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
+        DisplayMap {
+            buffer,
+            buffer_subscription,
+            fold_map,
+            inlay_map,
+            tab_map,
+            wrap_map,
+            block_map,
+            text_highlights: Default::default(),
+            inlay_highlights: Default::default(),
+            clip_at_line_ends: false,
+        }
+    }
+
+    pub fn snapshot(&mut self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
+        let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+        let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
+        let (wrap_snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
+        let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits);
+
+        DisplaySnapshot {
+            buffer_snapshot: self.buffer.read(cx).snapshot(cx),
+            fold_snapshot,
+            inlay_snapshot,
+            tab_snapshot,
+            wrap_snapshot,
+            block_snapshot,
+            text_highlights: self.text_highlights.clone(),
+            inlay_highlights: self.inlay_highlights.clone(),
+            clip_at_line_ends: self.clip_at_line_ends,
+        }
+    }
+
+    pub fn set_state(&mut self, other: &DisplaySnapshot, cx: &mut ModelContext<Self>) {
+        self.fold(
+            other
+                .folds_in_range(0..other.buffer_snapshot.len())
+                .map(|fold| fold.to_offset(&other.buffer_snapshot)),
+            cx,
+        );
+    }
+
+    pub fn fold<T: ToOffset>(
+        &mut self,
+        ranges: impl IntoIterator<Item = Range<T>>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
+        let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+        let (snapshot, edits) = fold_map.fold(ranges);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+    }
+
+    pub fn unfold<T: ToOffset>(
+        &mut self,
+        ranges: impl IntoIterator<Item = Range<T>>,
+        inclusive: bool,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
+        let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+        let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+    }
+
+    pub fn insert_blocks(
+        &mut self,
+        blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Vec<BlockId> {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        let mut block_map = self.block_map.write(snapshot, edits);
+        block_map.insert(blocks)
+    }
+
+    pub fn replace_blocks(&mut self, styles: HashMap<BlockId, RenderBlock>) {
+        self.block_map.replace(styles);
+    }
+
+    pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        let mut block_map = self.block_map.write(snapshot, edits);
+        block_map.remove(ids);
+    }
+
+    pub fn highlight_text(
+        &mut self,
+        type_id: TypeId,
+        ranges: Vec<Range<Anchor>>,
+        style: HighlightStyle,
+    ) {
+        self.text_highlights
+            .insert(Some(type_id), Arc::new((style, ranges)));
+    }
+
+    pub fn highlight_inlays(
+        &mut self,
+        type_id: TypeId,
+        highlights: Vec<InlayHighlight>,
+        style: HighlightStyle,
+    ) {
+        for highlight in highlights {
+            self.inlay_highlights
+                .entry(type_id)
+                .or_default()
+                .insert(highlight.inlay, (style, highlight));
+        }
+    }
+
+    pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
+        let highlights = self.text_highlights.get(&Some(type_id))?;
+        Some((highlights.0, &highlights.1))
+    }
+    pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
+        let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some();
+        cleared |= self.inlay_highlights.remove(&type_id).is_none();
+        cleared
+    }
+
+    pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
+        self.wrap_map
+            .update(cx, |map, cx| map.set_font(font_id, font_size, cx))
+    }
+
+    pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool {
+        self.fold_map.set_ellipses_color(color)
+    }
+
+    pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
+        self.wrap_map
+            .update(cx, |map, cx| map.set_wrap_width(width, cx))
+    }
+
+    pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+        self.inlay_map.current_inlays()
+    }
+
+    pub fn splice_inlays(
+        &mut self,
+        to_remove: Vec<InlayId>,
+        to_insert: Vec<Inlay>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if to_remove.is_empty() && to_insert.is_empty() {
+            return;
+        }
+        let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+
+        let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+    }
+
+    fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
+        let language = buffer
+            .read(cx)
+            .as_singleton()
+            .and_then(|buffer| buffer.read(cx).language());
+        language_settings(language.as_deref(), None, cx).tab_size
+    }
+
+    #[cfg(test)]
+    pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
+        self.wrap_map.read(cx).is_rewrapping()
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct Highlights<'a> {
+    pub text_highlights: Option<&'a TextHighlights>,
+    pub inlay_highlights: Option<&'a InlayHighlights>,
+    pub inlay_highlight_style: Option<HighlightStyle>,
+    pub suggestion_highlight_style: Option<HighlightStyle>,
+}
+
+pub struct HighlightedChunk<'a> {
+    pub chunk: &'a str,
+    pub style: Option<HighlightStyle>,
+    pub is_tab: bool,
+}
+
+pub struct DisplaySnapshot {
+    pub buffer_snapshot: MultiBufferSnapshot,
+    pub fold_snapshot: fold_map::FoldSnapshot,
+    inlay_snapshot: inlay_map::InlaySnapshot,
+    tab_snapshot: tab_map::TabSnapshot,
+    wrap_snapshot: wrap_map::WrapSnapshot,
+    block_snapshot: block_map::BlockSnapshot,
+    text_highlights: TextHighlights,
+    inlay_highlights: InlayHighlights,
+    clip_at_line_ends: bool,
+}
+
+impl DisplaySnapshot {
+    #[cfg(test)]
+    pub fn fold_count(&self) -> usize {
+        self.fold_snapshot.fold_count()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.buffer_snapshot.len() == 0
+    }
+
+    pub fn buffer_rows(&self, start_row: u32) -> DisplayBufferRows {
+        self.block_snapshot.buffer_rows(start_row)
+    }
+
+    pub fn max_buffer_row(&self) -> u32 {
+        self.buffer_snapshot.max_buffer_row()
+    }
+
+    pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
+        loop {
+            let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
+            let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);
+            fold_point.0.column = 0;
+            inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+            point = self.inlay_snapshot.to_buffer_point(inlay_point);
+
+            let mut display_point = self.point_to_display_point(point, Bias::Left);
+            *display_point.column_mut() = 0;
+            let next_point = self.display_point_to_point(display_point, Bias::Left);
+            if next_point == point {
+                return (point, display_point);
+            }
+            point = next_point;
+        }
+    }
+
+    pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
+        loop {
+            let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
+            let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
+            fold_point.0.column = self.fold_snapshot.line_len(fold_point.row());
+            inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+            point = self.inlay_snapshot.to_buffer_point(inlay_point);
+
+            let mut display_point = self.point_to_display_point(point, Bias::Right);
+            *display_point.column_mut() = self.line_len(display_point.row());
+            let next_point = self.display_point_to_point(display_point, Bias::Right);
+            if next_point == point {
+                return (point, display_point);
+            }
+            point = next_point;
+        }
+    }
+
+    // used by line_mode selections and tries to match vim behaviour
+    pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
+        let new_start = if range.start.row == 0 {
+            Point::new(0, 0)
+        } else if range.start.row == self.max_buffer_row()
+            || (range.end.column > 0 && range.end.row == self.max_buffer_row())
+        {
+            Point::new(range.start.row - 1, self.line_len(range.start.row - 1))
+        } else {
+            self.prev_line_boundary(range.start).0
+        };
+
+        let new_end = if range.end.column == 0 {
+            range.end
+        } else if range.end.row < self.max_buffer_row() {
+            self.buffer_snapshot
+                .clip_point(Point::new(range.end.row + 1, 0), Bias::Left)
+        } else {
+            self.buffer_snapshot.max_point()
+        };
+
+        new_start..new_end
+    }
+
+    fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
+        let inlay_point = self.inlay_snapshot.to_inlay_point(point);
+        let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+        let tab_point = self.tab_snapshot.to_tab_point(fold_point);
+        let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
+        let block_point = self.block_snapshot.to_block_point(wrap_point);
+        DisplayPoint(block_point)
+    }
+
+    fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
+        self.inlay_snapshot
+            .to_buffer_point(self.display_point_to_inlay_point(point, bias))
+    }
+
+    pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset {
+        self.inlay_snapshot
+            .to_offset(self.display_point_to_inlay_point(point, bias))
+    }
+
+    pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset {
+        self.inlay_snapshot
+            .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
+    }
+
+    fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
+        let block_point = point.0;
+        let wrap_point = self.block_snapshot.to_wrap_point(block_point);
+        let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
+        let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
+        fold_point.to_inlay_point(&self.fold_snapshot)
+    }
+
+    pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
+        let block_point = point.0;
+        let wrap_point = self.block_snapshot.to_wrap_point(block_point);
+        let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
+        self.tab_snapshot.to_fold_point(tab_point, bias).0
+    }
+
+    pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
+        let tab_point = self.tab_snapshot.to_tab_point(fold_point);
+        let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
+        let block_point = self.block_snapshot.to_block_point(wrap_point);
+        DisplayPoint(block_point)
+    }
+
+    pub fn max_point(&self) -> DisplayPoint {
+        DisplayPoint(self.block_snapshot.max_point())
+    }
+
+    /// Returns text chunks starting at the given display row until the end of the file
+    pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
+        self.block_snapshot
+            .chunks(
+                display_row..self.max_point().row() + 1,
+                false,
+                Highlights::default(),
+            )
+            .map(|h| h.text)
+    }
+
+    /// Returns text chunks starting at the end of the given display row in reverse until the start of the file
+    pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
+        (0..=display_row).into_iter().rev().flat_map(|row| {
+            self.block_snapshot
+                .chunks(row..row + 1, false, Highlights::default())
+                .map(|h| h.text)
+                .collect::<Vec<_>>()
+                .into_iter()
+                .rev()
+        })
+    }
+
+    pub fn chunks<'a>(
+        &'a self,
+        display_rows: Range<u32>,
+        language_aware: bool,
+        inlay_highlight_style: Option<HighlightStyle>,
+        suggestion_highlight_style: Option<HighlightStyle>,
+    ) -> DisplayChunks<'a> {
+        self.block_snapshot.chunks(
+            display_rows,
+            language_aware,
+            Highlights {
+                text_highlights: Some(&self.text_highlights),
+                inlay_highlights: Some(&self.inlay_highlights),
+                inlay_highlight_style,
+                suggestion_highlight_style,
+            },
+        )
+    }
+
+    pub fn highlighted_chunks<'a>(
+        &'a self,
+        display_rows: Range<u32>,
+        language_aware: bool,
+        style: &'a EditorStyle,
+    ) -> impl Iterator<Item = HighlightedChunk<'a>> {
+        self.chunks(
+            display_rows,
+            language_aware,
+            Some(style.theme.hint),
+            Some(style.theme.suggestion),
+        )
+        .map(|chunk| {
+            let mut highlight_style = chunk
+                .syntax_highlight_id
+                .and_then(|id| id.style(&style.syntax));
+
+            if let Some(chunk_highlight) = chunk.highlight_style {
+                if let Some(highlight_style) = highlight_style.as_mut() {
+                    highlight_style.highlight(chunk_highlight);
+                } else {
+                    highlight_style = Some(chunk_highlight);
+                }
+            }
+
+            let mut diagnostic_highlight = HighlightStyle::default();
+
+            if chunk.is_unnecessary {
+                diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
+            }
+
+            if let Some(severity) = chunk.diagnostic_severity {
+                // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
+                if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
+                    let diagnostic_style = super::diagnostic_style(severity, true, style);
+                    diagnostic_highlight.underline = Some(Underline {
+                        color: Some(diagnostic_style.message.text.color),
+                        thickness: 1.0.into(),
+                        squiggly: true,
+                    });
+                }
+            }
+
+            if let Some(highlight_style) = highlight_style.as_mut() {
+                highlight_style.highlight(diagnostic_highlight);
+            } else {
+                highlight_style = Some(diagnostic_highlight);
+            }
+
+            HighlightedChunk {
+                chunk: chunk.text,
+                style: highlight_style,
+                is_tab: chunk.is_tab,
+            }
+        })
+    }
+
+    pub fn lay_out_line_for_row(
+        &self,
+        display_row: u32,
+        TextLayoutDetails {
+            font_cache,
+            text_layout_cache,
+            editor_style,
+        }: &TextLayoutDetails,
+    ) -> Line {
+        let mut styles = Vec::new();
+        let mut line = String::new();
+        let mut ended_in_newline = false;
+
+        let range = display_row..display_row + 1;
+        for chunk in self.highlighted_chunks(range, false, editor_style) {
+            line.push_str(chunk.chunk);
+
+            let text_style = if let Some(style) = chunk.style {
+                editor_style
+                    .text
+                    .clone()
+                    .highlight(style, font_cache)
+                    .map(Cow::Owned)
+                    .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
+            } else {
+                Cow::Borrowed(&editor_style.text)
+            };
+            ended_in_newline = chunk.chunk.ends_with("\n");
+
+            styles.push((
+                chunk.chunk.len(),
+                RunStyle {
+                    font_id: text_style.font_id,
+                    color: text_style.color,
+                    underline: text_style.underline,
+                },
+            ));
+        }
+
+        // our pixel positioning logic assumes each line ends in \n,
+        // this is almost always true except for the last line which
+        // may have no trailing newline.
+        if !ended_in_newline && display_row == self.max_point().row() {
+            line.push_str("\n");
+
+            styles.push((
+                "\n".len(),
+                RunStyle {
+                    font_id: editor_style.text.font_id,
+                    color: editor_style.text_color,
+                    underline: editor_style.text.underline,
+                },
+            ));
+        }
+
+        text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles)
+    }
+
+    pub fn x_for_point(
+        &self,
+        display_point: DisplayPoint,
+        text_layout_details: &TextLayoutDetails,
+    ) -> f32 {
+        let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
+        layout_line.x_for_index(display_point.column() as usize)
+    }
+
+    pub fn column_for_x(
+        &self,
+        display_row: u32,
+        x_coordinate: f32,
+        text_layout_details: &TextLayoutDetails,
+    ) -> u32 {
+        let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
+        layout_line.closest_index_for_x(x_coordinate) as u32
+    }
+
+    pub fn chars_at(
+        &self,
+        mut point: DisplayPoint,
+    ) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
+        point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
+        self.text_chunks(point.row())
+            .flat_map(str::chars)
+            .skip_while({
+                let mut column = 0;
+                move |char| {
+                    let at_point = column >= point.column();
+                    column += char.len_utf8() as u32;
+                    !at_point
+                }
+            })
+            .map(move |ch| {
+                let result = (ch, point);
+                if ch == '\n' {
+                    *point.row_mut() += 1;
+                    *point.column_mut() = 0;
+                } else {
+                    *point.column_mut() += ch.len_utf8() as u32;
+                }
+                result
+            })
+    }
+
+    pub fn reverse_chars_at(
+        &self,
+        mut point: DisplayPoint,
+    ) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
+        point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
+        self.reverse_text_chunks(point.row())
+            .flat_map(|chunk| chunk.chars().rev())
+            .skip_while({
+                let mut column = self.line_len(point.row());
+                if self.max_point().row() > point.row() {
+                    column += 1;
+                }
+
+                move |char| {
+                    let at_point = column <= point.column();
+                    column = column.saturating_sub(char.len_utf8() as u32);
+                    !at_point
+                }
+            })
+            .map(move |ch| {
+                if ch == '\n' {
+                    *point.row_mut() -= 1;
+                    *point.column_mut() = self.line_len(point.row());
+                } else {
+                    *point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
+                }
+                (ch, point)
+            })
+    }
+
+    pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
+        let mut count = 0;
+        let mut column = 0;
+        for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
+            if column >= target {
+                break;
+            }
+            count += 1;
+            column += c.len_utf8() as u32;
+        }
+        count
+    }
+
+    pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
+        let mut column = 0;
+
+        for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
+            if c == '\n' || count >= char_count as usize {
+                break;
+            }
+            column += c.len_utf8() as u32;
+        }
+
+        column
+    }
+
+    pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
+        let mut clipped = self.block_snapshot.clip_point(point.0, bias);
+        if self.clip_at_line_ends {
+            clipped = self.clip_at_line_end(DisplayPoint(clipped)).0
+        }
+        DisplayPoint(clipped)
+    }
+
+    pub fn clip_at_line_end(&self, point: DisplayPoint) -> DisplayPoint {
+        let mut point = point.0;
+        if point.column == self.line_len(point.row) {
+            point.column = point.column.saturating_sub(1);
+            point = self.block_snapshot.clip_point(point, Bias::Left);
+        }
+        DisplayPoint(point)
+    }
+
+    pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
+    where
+        T: ToOffset,
+    {
+        self.fold_snapshot.folds_in_range(range)
+    }
+
+    pub fn blocks_in_range(
+        &self,
+        rows: Range<u32>,
+    ) -> impl Iterator<Item = (u32, &TransformBlock)> {
+        self.block_snapshot.blocks_in_range(rows)
+    }
+
+    pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
+        self.fold_snapshot.intersects_fold(offset)
+    }
+
+    pub fn is_line_folded(&self, buffer_row: u32) -> bool {
+        self.fold_snapshot.is_line_folded(buffer_row)
+    }
+
+    pub fn is_block_line(&self, display_row: u32) -> bool {
+        self.block_snapshot.is_block_line(display_row)
+    }
+
+    pub fn soft_wrap_indent(&self, display_row: u32) -> Option<u32> {
+        let wrap_row = self
+            .block_snapshot
+            .to_wrap_point(BlockPoint::new(display_row, 0))
+            .row();
+        self.wrap_snapshot.soft_wrap_indent(wrap_row)
+    }
+
+    pub fn text(&self) -> String {
+        self.text_chunks(0).collect()
+    }
+
+    pub fn line(&self, display_row: u32) -> String {
+        let mut result = String::new();
+        for chunk in self.text_chunks(display_row) {
+            if let Some(ix) = chunk.find('\n') {
+                result.push_str(&chunk[0..ix]);
+                break;
+            } else {
+                result.push_str(chunk);
+            }
+        }
+        result
+    }
+
+    pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
+        let mut indent = 0;
+        let mut is_blank = true;
+        for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
+            if c == ' ' {
+                indent += 1;
+            } else {
+                is_blank = c == '\n';
+                break;
+            }
+        }
+        (indent, is_blank)
+    }
+
+    pub fn line_indent_for_buffer_row(&self, buffer_row: u32) -> (u32, bool) {
+        let (buffer, range) = self
+            .buffer_snapshot
+            .buffer_line_for_row(buffer_row)
+            .unwrap();
+
+        let mut indent_size = 0;
+        let mut is_blank = false;
+        for c in buffer.chars_at(Point::new(range.start.row, 0)) {
+            if c == ' ' || c == '\t' {
+                indent_size += 1;
+            } else {
+                if c == '\n' {
+                    is_blank = true;
+                }
+                break;
+            }
+        }
+
+        (indent_size, is_blank)
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        self.block_snapshot.line_len(row)
+    }
+
+    pub fn longest_row(&self) -> u32 {
+        self.block_snapshot.longest_row()
+    }
+
+    pub fn fold_for_line(self: &Self, buffer_row: u32) -> Option<FoldStatus> {
+        if self.is_line_folded(buffer_row) {
+            Some(FoldStatus::Folded)
+        } else if self.is_foldable(buffer_row) {
+            Some(FoldStatus::Foldable)
+        } else {
+            None
+        }
+    }
+
+    pub fn is_foldable(self: &Self, buffer_row: u32) -> bool {
+        let max_row = self.buffer_snapshot.max_buffer_row();
+        if buffer_row >= max_row {
+            return false;
+        }
+
+        let (indent_size, is_blank) = self.line_indent_for_buffer_row(buffer_row);
+        if is_blank {
+            return false;
+        }
+
+        for next_row in (buffer_row + 1)..=max_row {
+            let (next_indent_size, next_line_is_blank) = self.line_indent_for_buffer_row(next_row);
+            if next_indent_size > indent_size {
+                return true;
+            } else if !next_line_is_blank {
+                break;
+            }
+        }
+
+        false
+    }
+
+    pub fn foldable_range(self: &Self, buffer_row: u32) -> Option<Range<Point>> {
+        let start = Point::new(buffer_row, self.buffer_snapshot.line_len(buffer_row));
+        if self.is_foldable(start.row) && !self.is_line_folded(start.row) {
+            let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row);
+            let max_point = self.buffer_snapshot.max_point();
+            let mut end = None;
+
+            for row in (buffer_row + 1)..=max_point.row {
+                let (indent, is_blank) = self.line_indent_for_buffer_row(row);
+                if !is_blank && indent <= start_indent {
+                    let prev_row = row - 1;
+                    end = Some(Point::new(
+                        prev_row,
+                        self.buffer_snapshot.line_len(prev_row),
+                    ));
+                    break;
+                }
+            }
+            let end = end.unwrap_or(max_point);
+            Some(start..end)
+        } else {
+            None
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn text_highlight_ranges<Tag: ?Sized + 'static>(
+        &self,
+    ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
+        let type_id = TypeId::of::<Tag>();
+        self.text_highlights.get(&Some(type_id)).cloned()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn inlay_highlights<Tag: ?Sized + 'static>(
+        &self,
+    ) -> Option<&HashMap<InlayId, (HighlightStyle, InlayHighlight)>> {
+        let type_id = TypeId::of::<Tag>();
+        self.inlay_highlights.get(&type_id)
+    }
+}
+
+#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct DisplayPoint(BlockPoint);
+
+impl Debug for DisplayPoint {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_fmt(format_args!(
+            "DisplayPoint({}, {})",
+            self.row(),
+            self.column()
+        ))
+    }
+}
+
+impl DisplayPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(BlockPoint(Point::new(row, column)))
+    }
+
+    pub fn zero() -> Self {
+        Self::new(0, 0)
+    }
+
+    pub fn is_zero(&self) -> bool {
+        self.0.is_zero()
+    }
+
+    pub fn row(self) -> u32 {
+        self.0.row
+    }
+
+    pub fn column(self) -> u32 {
+        self.0.column
+    }
+
+    pub fn row_mut(&mut self) -> &mut u32 {
+        &mut self.0.row
+    }
+
+    pub fn column_mut(&mut self) -> &mut u32 {
+        &mut self.0.column
+    }
+
+    pub fn to_point(self, map: &DisplaySnapshot) -> Point {
+        map.display_point_to_point(self, Bias::Left)
+    }
+
+    pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
+        let wrap_point = map.block_snapshot.to_wrap_point(self.0);
+        let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
+        let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
+        let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
+        map.inlay_snapshot
+            .to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
+    }
+}
+
+impl ToDisplayPoint for usize {
+    fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
+        map.point_to_display_point(self.to_point(&map.buffer_snapshot), Bias::Left)
+    }
+}
+
+impl ToDisplayPoint for OffsetUtf16 {
+    fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
+        self.to_offset(&map.buffer_snapshot).to_display_point(map)
+    }
+}
+
+impl ToDisplayPoint for Point {
+    fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
+        map.point_to_display_point(*self, Bias::Left)
+    }
+}
+
+impl ToDisplayPoint for Anchor {
+    fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
+        self.to_point(&map.buffer_snapshot).to_display_point(map)
+    }
+}
+
+pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator<Item = u32> {
+    let max_row = display_map.max_point().row();
+    let start_row = display_row + 1;
+    let mut current = None;
+    std::iter::from_fn(move || {
+        if current == None {
+            current = Some(start_row);
+        } else {
+            current = Some(current.unwrap() + 1)
+        }
+        if current.unwrap() > max_row {
+            None
+        } else {
+            current
+        }
+    })
+}
+
+#[cfg(test)]
+pub mod tests {
+    use super::*;
+    use crate::{
+        movement,
+        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+    };
+    use gpui::{color::Color, elements::*, test::observe, AppContext};
+    use language::{
+        language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
+        Buffer, Language, LanguageConfig, SelectionGoal,
+    };
+    use project::Project;
+    use rand::{prelude::*, Rng};
+    use settings::SettingsStore;
+    use smol::stream::StreamExt;
+    use std::{env, sync::Arc};
+    use theme::SyntaxTheme;
+    use util::test::{marked_text_ranges, sample_text};
+    use Bias::*;
+
+    #[gpui::test(iterations = 100)]
+    async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+        cx.foreground().set_block_on_ticks(0..=50);
+        cx.foreground().forbid_parking();
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let font_cache = cx.font_cache().clone();
+        let mut tab_size = rng.gen_range(1..=4);
+        let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
+        let excerpt_header_height = rng.gen_range(1..=5);
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let max_wrap_width = 300.0;
+        let mut wrap_width = if rng.gen_bool(0.1) {
+            None
+        } else {
+            Some(rng.gen_range(0.0..=max_wrap_width))
+        };
+
+        log::info!("tab size: {}", tab_size);
+        log::info!("wrap width: {:?}", wrap_width);
+
+        cx.update(|cx| {
+            init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
+        });
+
+        let buffer = cx.update(|cx| {
+            if rng.gen() {
+                let len = rng.gen_range(0..10);
+                let text = util::RandomCharIter::new(&mut rng)
+                    .take(len)
+                    .collect::<String>();
+                MultiBuffer::build_simple(&text, cx)
+            } else {
+                MultiBuffer::build_random(&mut rng, cx)
+            }
+        });
+
+        let map = cx.add_model(|cx| {
+            DisplayMap::new(
+                buffer.clone(),
+                font_id,
+                font_size,
+                wrap_width,
+                buffer_start_excerpt_header_height,
+                excerpt_header_height,
+                cx,
+            )
+        });
+        let mut notifications = observe(&map, cx);
+        let mut fold_count = 0;
+        let mut blocks = Vec::new();
+
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+        log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
+        log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
+        log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
+        log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
+        log::info!("block text: {:?}", snapshot.block_snapshot.text());
+        log::info!("display text: {:?}", snapshot.text());
+
+        for _i in 0..operations {
+            match rng.gen_range(0..100) {
+                0..=19 => {
+                    wrap_width = if rng.gen_bool(0.2) {
+                        None
+                    } else {
+                        Some(rng.gen_range(0.0..=max_wrap_width))
+                    };
+                    log::info!("setting wrap width to {:?}", wrap_width);
+                    map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+                }
+                20..=29 => {
+                    let mut tab_sizes = vec![1, 2, 3, 4];
+                    tab_sizes.remove((tab_size - 1) as usize);
+                    tab_size = *tab_sizes.choose(&mut rng).unwrap();
+                    log::info!("setting tab size to {:?}", tab_size);
+                    cx.update(|cx| {
+                        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                            store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                                s.defaults.tab_size = NonZeroU32::new(tab_size);
+                            });
+                        });
+                    });
+                }
+                30..=44 => {
+                    map.update(cx, |map, cx| {
+                        if rng.gen() || blocks.is_empty() {
+                            let buffer = map.snapshot(cx).buffer_snapshot;
+                            let block_properties = (0..rng.gen_range(1..=1))
+                                .map(|_| {
+                                    let position =
+                                        buffer.anchor_after(buffer.clip_offset(
+                                            rng.gen_range(0..=buffer.len()),
+                                            Bias::Left,
+                                        ));
+
+                                    let disposition = if rng.gen() {
+                                        BlockDisposition::Above
+                                    } else {
+                                        BlockDisposition::Below
+                                    };
+                                    let height = rng.gen_range(1..5);
+                                    log::info!(
+                                        "inserting block {:?} {:?} with height {}",
+                                        disposition,
+                                        position.to_point(&buffer),
+                                        height
+                                    );
+                                    BlockProperties {
+                                        style: BlockStyle::Fixed,
+                                        position,
+                                        height,
+                                        disposition,
+                                        render: Arc::new(|_| Empty::new().into_any()),
+                                    }
+                                })
+                                .collect::<Vec<_>>();
+                            blocks.extend(map.insert_blocks(block_properties, cx));
+                        } else {
+                            blocks.shuffle(&mut rng);
+                            let remove_count = rng.gen_range(1..=4.min(blocks.len()));
+                            let block_ids_to_remove = (0..remove_count)
+                                .map(|_| blocks.remove(rng.gen_range(0..blocks.len())))
+                                .collect();
+                            log::info!("removing block ids {:?}", block_ids_to_remove);
+                            map.remove_blocks(block_ids_to_remove, cx);
+                        }
+                    });
+                }
+                45..=79 => {
+                    let mut ranges = Vec::new();
+                    for _ in 0..rng.gen_range(1..=3) {
+                        buffer.read_with(cx, |buffer, cx| {
+                            let buffer = buffer.read(cx);
+                            let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
+                            let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+                            ranges.push(start..end);
+                        });
+                    }
+
+                    if rng.gen() && fold_count > 0 {
+                        log::info!("unfolding ranges: {:?}", ranges);
+                        map.update(cx, |map, cx| {
+                            map.unfold(ranges, true, cx);
+                        });
+                    } else {
+                        log::info!("folding ranges: {:?}", ranges);
+                        map.update(cx, |map, cx| {
+                            map.fold(ranges, cx);
+                        });
+                    }
+                }
+                _ => {
+                    buffer.update(cx, |buffer, cx| buffer.randomly_mutate(&mut rng, 5, cx));
+                }
+            }
+
+            if map.read_with(cx, |map, cx| map.is_rewrapping(cx)) {
+                notifications.next().await.unwrap();
+            }
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            fold_count = snapshot.fold_count();
+            log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
+            log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
+            log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
+            log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
+            log::info!("block text: {:?}", snapshot.block_snapshot.text());
+            log::info!("display text: {:?}", snapshot.text());
+
+            // Line boundaries
+            let buffer = &snapshot.buffer_snapshot;
+            for _ in 0..5 {
+                let row = rng.gen_range(0..=buffer.max_point().row);
+                let column = rng.gen_range(0..=buffer.line_len(row));
+                let point = buffer.clip_point(Point::new(row, column), Left);
+
+                let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point);
+                let (next_buffer_bound, next_display_bound) = snapshot.next_line_boundary(point);
+
+                assert!(prev_buffer_bound <= point);
+                assert!(next_buffer_bound >= point);
+                assert_eq!(prev_buffer_bound.column, 0);
+                assert_eq!(prev_display_bound.column(), 0);
+                if next_buffer_bound < buffer.max_point() {
+                    assert_eq!(buffer.chars_at(next_buffer_bound).next(), Some('\n'));
+                }
+
+                assert_eq!(
+                    prev_display_bound,
+                    prev_buffer_bound.to_display_point(&snapshot),
+                    "row boundary before {:?}. reported buffer row boundary: {:?}",
+                    point,
+                    prev_buffer_bound
+                );
+                assert_eq!(
+                    next_display_bound,
+                    next_buffer_bound.to_display_point(&snapshot),
+                    "display row boundary after {:?}. reported buffer row boundary: {:?}",
+                    point,
+                    next_buffer_bound
+                );
+                assert_eq!(
+                    prev_buffer_bound,
+                    prev_display_bound.to_point(&snapshot),
+                    "row boundary before {:?}. reported display row boundary: {:?}",
+                    point,
+                    prev_display_bound
+                );
+                assert_eq!(
+                    next_buffer_bound,
+                    next_display_bound.to_point(&snapshot),
+                    "row boundary after {:?}. reported display row boundary: {:?}",
+                    point,
+                    next_display_bound
+                );
+            }
+
+            // Movement
+            let min_point = snapshot.clip_point(DisplayPoint::new(0, 0), Left);
+            let max_point = snapshot.clip_point(snapshot.max_point(), Right);
+            for _ in 0..5 {
+                let row = rng.gen_range(0..=snapshot.max_point().row());
+                let column = rng.gen_range(0..=snapshot.line_len(row));
+                let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
+
+                log::info!("Moving from point {:?}", point);
+
+                let moved_right = movement::right(&snapshot, point);
+                log::info!("Right {:?}", moved_right);
+                if point < max_point {
+                    assert!(moved_right > point);
+                    if point.column() == snapshot.line_len(point.row())
+                        || snapshot.soft_wrap_indent(point.row()).is_some()
+                            && point.column() == snapshot.line_len(point.row()) - 1
+                    {
+                        assert!(moved_right.row() > point.row());
+                    }
+                } else {
+                    assert_eq!(moved_right, point);
+                }
+
+                let moved_left = movement::left(&snapshot, point);
+                log::info!("Left {:?}", moved_left);
+                if point > min_point {
+                    assert!(moved_left < point);
+                    if point.column() == 0 {
+                        assert!(moved_left.row() < point.row());
+                    }
+                } else {
+                    assert_eq!(moved_left, point);
+                }
+            }
+        }
+    }
+
+    #[gpui::test(retries = 5)]
+    async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
+        cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
+        cx.update(|cx| {
+            init_test(cx, |_| {});
+        });
+
+        let mut cx = EditorTestContext::new(cx).await;
+        let editor = cx.editor.clone();
+        let window = cx.window.clone();
+
+        cx.update_window(window, |cx| {
+            let text_layout_details =
+                editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
+
+            let font_cache = cx.font_cache().clone();
+
+            let family_id = font_cache
+                .load_family(&["Helvetica"], &Default::default())
+                .unwrap();
+            let font_id = font_cache
+                .select_font(family_id, &Default::default())
+                .unwrap();
+            let font_size = 12.0;
+            let wrap_width = Some(64.);
+
+            let text = "one two three four five\nsix seven eight";
+            let buffer = MultiBuffer::build_simple(text, cx);
+            let map = cx.add_model(|cx| {
+                DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
+            });
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(0).collect::<String>(),
+                "one two \nthree four \nfive\nsix seven \neight"
+            );
+            assert_eq!(
+                snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
+                DisplayPoint::new(0, 7)
+            );
+            assert_eq!(
+                snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
+                DisplayPoint::new(1, 0)
+            );
+            assert_eq!(
+                movement::right(&snapshot, DisplayPoint::new(0, 7)),
+                DisplayPoint::new(1, 0)
+            );
+            assert_eq!(
+                movement::left(&snapshot, DisplayPoint::new(1, 0)),
+                DisplayPoint::new(0, 7)
+            );
+
+            let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details);
+            assert_eq!(
+                movement::up(
+                    &snapshot,
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::None,
+                    false,
+                    &text_layout_details,
+                ),
+                (
+                    DisplayPoint::new(0, 7),
+                    SelectionGoal::HorizontalPosition(x)
+                )
+            );
+            assert_eq!(
+                movement::down(
+                    &snapshot,
+                    DisplayPoint::new(0, 7),
+                    SelectionGoal::HorizontalPosition(x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::HorizontalPosition(x)
+                )
+            );
+            assert_eq!(
+                movement::down(
+                    &snapshot,
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::HorizontalPosition(x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 4),
+                    SelectionGoal::HorizontalPosition(x)
+                )
+            );
+
+            let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
+            buffer.update(cx, |buffer, cx| {
+                buffer.edit([(ix..ix, "and ")], None, cx);
+            });
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(1).collect::<String>(),
+                "three four \nfive\nsix and \nseven eight"
+            );
+
+            // Re-wrap on font size changes
+            map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(1).collect::<String>(),
+                "three \nfour five\nsix and \nseven \neight"
+            )
+        });
+    }
+
+    #[gpui::test]
+    fn test_text_chunks(cx: &mut gpui::AppContext) {
+        init_test(cx, |_| {});
+
+        let text = sample_text(6, 6, 'a');
+        let buffer = MultiBuffer::build_simple(&text, cx);
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
+
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit(
+                vec![
+                    (Point::new(1, 0)..Point::new(1, 0), "\t"),
+                    (Point::new(1, 1)..Point::new(1, 1), "\t"),
+                    (Point::new(2, 1)..Point::new(2, 1), "\t"),
+                ],
+                None,
+                cx,
+            )
+        });
+
+        assert_eq!(
+            map.update(cx, |map, cx| map.snapshot(cx))
+                .text_chunks(1)
+                .collect::<String>()
+                .lines()
+                .next(),
+            Some("    b   bbbbb")
+        );
+        assert_eq!(
+            map.update(cx, |map, cx| map.snapshot(cx))
+                .text_chunks(2)
+                .collect::<String>()
+                .lines()
+                .next(),
+            Some("c   ccccc")
+        );
+    }
+
+    #[gpui::test]
+    async fn test_chunks(cx: &mut gpui::TestAppContext) {
+        use unindent::Unindent as _;
+
+        let text = r#"
+            fn outer() {}
+
+            mod module {
+                fn inner() {}
+            }"#
+        .unindent();
+
+        let theme = SyntaxTheme::new(vec![
+            ("mod.body".to_string(), Color::red().into()),
+            ("fn.name".to_string(), Color::blue().into()),
+        ]);
+        let language = Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Test".into(),
+                    path_suffixes: vec![".test".to_string()],
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )
+            .with_highlights_query(
+                r#"
+                (mod_item name: (identifier) body: _ @mod.body)
+                (function_item name: (identifier) @fn.name)
+                "#,
+            )
+            .unwrap(),
+        );
+        language.set_theme(&theme);
+
+        cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
+
+        let buffer = cx
+            .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+        buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+
+        let font_cache = cx.font_cache();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+
+        let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
+            vec![
+                ("fn ".to_string(), None),
+                ("outer".to_string(), Some(Color::blue())),
+                ("() {}\n\nmod module ".to_string(), None),
+                ("{\n    fn ".to_string(), Some(Color::red())),
+                ("inner".to_string(), Some(Color::blue())),
+                ("() {}\n}".to_string(), Some(Color::red())),
+            ]
+        );
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
+            vec![
+                ("    fn ".to_string(), Some(Color::red())),
+                ("inner".to_string(), Some(Color::blue())),
+                ("() {}\n}".to_string(), Some(Color::red())),
+            ]
+        );
+
+        map.update(cx, |map, cx| {
+            map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
+        });
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
+            vec![
+                ("fn ".to_string(), None),
+                ("out".to_string(), Some(Color::blue())),
+                ("โ‹ฏ".to_string(), None),
+                ("  fn ".to_string(), Some(Color::red())),
+                ("inner".to_string(), Some(Color::blue())),
+                ("() {}\n}".to_string(), Some(Color::red())),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
+        use unindent::Unindent as _;
+
+        cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
+
+        let text = r#"
+            fn outer() {}
+
+            mod module {
+                fn inner() {}
+            }"#
+        .unindent();
+
+        let theme = SyntaxTheme::new(vec![
+            ("mod.body".to_string(), Color::red().into()),
+            ("fn.name".to_string(), Color::blue().into()),
+        ]);
+        let language = Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Test".into(),
+                    path_suffixes: vec![".test".to_string()],
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )
+            .with_highlights_query(
+                r#"
+                (mod_item name: (identifier) body: _ @mod.body)
+                (function_item name: (identifier) @fn.name)
+                "#,
+            )
+            .unwrap(),
+        );
+        language.set_theme(&theme);
+
+        cx.update(|cx| init_test(cx, |_| {}));
+
+        let buffer = cx
+            .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+        buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+
+        let font_cache = cx.font_cache();
+
+        let family_id = font_cache
+            .load_family(&["Courier"], &Default::default())
+            .unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 16.0;
+
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx));
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
+            [
+                ("fn \n".to_string(), None),
+                ("oute\nr".to_string(), Some(Color::blue())),
+                ("() \n{}\n\n".to_string(), None),
+            ]
+        );
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
+            [("{}\n\n".to_string(), None)]
+        );
+
+        map.update(cx, |map, cx| {
+            map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
+        });
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
+            [
+                ("out".to_string(), Some(Color::blue())),
+                ("โ‹ฏ\n".to_string(), None),
+                ("  \nfn ".to_string(), Some(Color::red())),
+                ("i\n".to_string(), Some(Color::blue()))
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| init_test(cx, |_| {}));
+
+        let theme = SyntaxTheme::new(vec![
+            ("operator".to_string(), Color::red().into()),
+            ("string".to_string(), Color::green().into()),
+        ]);
+        let language = Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Test".into(),
+                    path_suffixes: vec![".test".to_string()],
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )
+            .with_highlights_query(
+                r#"
+                ":" @operator
+                (string_literal) @string
+                "#,
+            )
+            .unwrap(),
+        );
+        language.set_theme(&theme);
+
+        let (text, highlighted_ranges) = marked_text_ranges(r#"constห‡ ยซaยป: B = "c ยซdยป""#, false);
+
+        let buffer = cx
+            .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+        buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
+
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+
+        let font_cache = cx.font_cache();
+        let family_id = font_cache
+            .load_family(&["Courier"], &Default::default())
+            .unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 16.0;
+        let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
+
+        enum MyType {}
+
+        let style = HighlightStyle {
+            color: Some(Color::blue()),
+            ..Default::default()
+        };
+
+        map.update(cx, |map, _cx| {
+            map.highlight_text(
+                TypeId::of::<MyType>(),
+                highlighted_ranges
+                    .into_iter()
+                    .map(|range| {
+                        buffer_snapshot.anchor_before(range.start)
+                            ..buffer_snapshot.anchor_before(range.end)
+                    })
+                    .collect(),
+                style,
+            );
+        });
+
+        assert_eq!(
+            cx.update(|cx| chunks(0..10, &map, &theme, cx)),
+            [
+                ("const ".to_string(), None, None),
+                ("a".to_string(), None, Some(Color::blue())),
+                (":".to_string(), Some(Color::red()), None),
+                (" B = ".to_string(), None, None),
+                ("\"c ".to_string(), Some(Color::green()), None),
+                ("d".to_string(), Some(Color::green()), Some(Color::blue())),
+                ("\"".to_string(), Some(Color::green()), None),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    fn test_clip_point(cx: &mut gpui::AppContext) {
+        init_test(cx, |_| {});
+
+        fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
+            let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
+
+            match bias {
+                Bias::Left => {
+                    if shift_right {
+                        *markers[1].column_mut() += 1;
+                    }
+
+                    assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
+                }
+                Bias::Right => {
+                    if shift_right {
+                        *markers[0].column_mut() += 1;
+                    }
+
+                    assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1])
+                }
+            };
+        }
+
+        use Bias::{Left, Right};
+        assert("ห‡ห‡ฮฑ", false, Left, cx);
+        assert("ห‡ห‡ฮฑ", true, Left, cx);
+        assert("ห‡ห‡ฮฑ", false, Right, cx);
+        assert("ห‡ฮฑห‡", true, Right, cx);
+        assert("ห‡ห‡โœ‹", false, Left, cx);
+        assert("ห‡ห‡โœ‹", true, Left, cx);
+        assert("ห‡ห‡โœ‹", false, Right, cx);
+        assert("ห‡โœ‹ห‡", true, Right, cx);
+        assert("ห‡ห‡๐Ÿ", false, Left, cx);
+        assert("ห‡ห‡๐Ÿ", true, Left, cx);
+        assert("ห‡ห‡๐Ÿ", false, Right, cx);
+        assert("ห‡๐Ÿห‡", true, Right, cx);
+        assert("ห‡ห‡\t", false, Left, cx);
+        assert("ห‡ห‡\t", true, Left, cx);
+        assert("ห‡ห‡\t", false, Right, cx);
+        assert("ห‡\tห‡", true, Right, cx);
+        assert(" ห‡ห‡\t", false, Left, cx);
+        assert(" ห‡ห‡\t", true, Left, cx);
+        assert(" ห‡ห‡\t", false, Right, cx);
+        assert(" ห‡\tห‡", true, Right, cx);
+        assert("   ห‡ห‡\t", false, Left, cx);
+        assert("   ห‡ห‡\t", false, Right, cx);
+    }
+
+    #[gpui::test]
+    fn test_clip_at_line_ends(cx: &mut gpui::AppContext) {
+        init_test(cx, |_| {});
+
+        fn assert(text: &str, cx: &mut gpui::AppContext) {
+            let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
+            unmarked_snapshot.clip_at_line_ends = true;
+            assert_eq!(
+                unmarked_snapshot.clip_point(markers[1], Bias::Left),
+                markers[0]
+            );
+        }
+
+        assert("ห‡ห‡", cx);
+        assert("ห‡aห‡", cx);
+        assert("aห‡bห‡", cx);
+        assert("aห‡ฮฑห‡", cx);
+    }
+
+    #[gpui::test]
+    fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
+        init_test(cx, |_| {});
+
+        let text = "โœ…\t\tฮฑ\nฮฒ\t\n๐Ÿ€ฮฒ\t\tฮณ";
+        let buffer = MultiBuffer::build_simple(text, cx);
+        let font_cache = cx.font_cache();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
+        let map = map.update(cx, |map, cx| map.snapshot(cx));
+        assert_eq!(map.text(), "โœ…       ฮฑ\nฮฒ   \n๐Ÿ€ฮฒ      ฮณ");
+        assert_eq!(
+            map.text_chunks(0).collect::<String>(),
+            "โœ…       ฮฑ\nฮฒ   \n๐Ÿ€ฮฒ      ฮณ"
+        );
+        assert_eq!(map.text_chunks(1).collect::<String>(), "ฮฒ   \n๐Ÿ€ฮฒ      ฮณ");
+        assert_eq!(map.text_chunks(2).collect::<String>(), "๐Ÿ€ฮฒ      ฮณ");
+
+        let point = Point::new(0, "โœ…\t\t".len() as u32);
+        let display_point = DisplayPoint::new(0, "โœ…       ".len() as u32);
+        assert_eq!(point.to_display_point(&map), display_point);
+        assert_eq!(display_point.to_point(&map), point);
+
+        let point = Point::new(1, "ฮฒ\t".len() as u32);
+        let display_point = DisplayPoint::new(1, "ฮฒ   ".len() as u32);
+        assert_eq!(point.to_display_point(&map), display_point);
+        assert_eq!(display_point.to_point(&map), point,);
+
+        let point = Point::new(2, "๐Ÿ€ฮฒ\t\t".len() as u32);
+        let display_point = DisplayPoint::new(2, "๐Ÿ€ฮฒ      ".len() as u32);
+        assert_eq!(point.to_display_point(&map), display_point);
+        assert_eq!(display_point.to_point(&map), point,);
+
+        // Display points inside of expanded tabs
+        assert_eq!(
+            DisplayPoint::new(0, "โœ…      ".len() as u32).to_point(&map),
+            Point::new(0, "โœ…\t".len() as u32),
+        );
+        assert_eq!(
+            DisplayPoint::new(0, "โœ… ".len() as u32).to_point(&map),
+            Point::new(0, "โœ…".len() as u32),
+        );
+
+        // Clipping display points inside of multi-byte characters
+        assert_eq!(
+            map.clip_point(DisplayPoint::new(0, "โœ…".len() as u32 - 1), Left),
+            DisplayPoint::new(0, 0)
+        );
+        assert_eq!(
+            map.clip_point(DisplayPoint::new(0, "โœ…".len() as u32 - 1), Bias::Right),
+            DisplayPoint::new(0, "โœ…".len() as u32)
+        );
+    }
+
+    #[gpui::test]
+    fn test_max_point(cx: &mut gpui::AppContext) {
+        init_test(cx, |_| {});
+
+        let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
+        let font_cache = cx.font_cache();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
+        assert_eq!(
+            map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
+            DisplayPoint::new(1, 11)
+        )
+    }
+
+    fn syntax_chunks<'a>(
+        rows: Range<u32>,
+        map: &ModelHandle<DisplayMap>,
+        theme: &'a SyntaxTheme,
+        cx: &mut AppContext,
+    ) -> Vec<(String, Option<Color>)> {
+        chunks(rows, map, theme, cx)
+            .into_iter()
+            .map(|(text, color, _)| (text, color))
+            .collect()
+    }
+
+    fn chunks<'a>(
+        rows: Range<u32>,
+        map: &ModelHandle<DisplayMap>,
+        theme: &'a SyntaxTheme,
+        cx: &mut AppContext,
+    ) -> Vec<(String, Option<Color>, Option<Color>)> {
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+        let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
+        for chunk in snapshot.chunks(rows, true, None, None) {
+            let syntax_color = chunk
+                .syntax_highlight_id
+                .and_then(|id| id.style(theme)?.color);
+            let highlight_color = chunk.highlight_style.and_then(|style| style.color);
+            if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
+                if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
+                    last_chunk.push_str(chunk.text);
+                    continue;
+                }
+            }
+            chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
+        }
+        chunks
+    }
+
+    fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+        cx.foreground().forbid_parking();
+        cx.set_global(SettingsStore::test(cx));
+        language::init(cx);
+        crate::init(cx);
+        Project::init_settings(cx);
+        theme::init((), cx);
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, f);
+        });
+    }
+}

crates/editor2/src/display_map/block_map.rs ๐Ÿ”—

@@ -0,0 +1,1667 @@
+use super::{
+    wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
+    Highlights,
+};
+use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
+use collections::{Bound, HashMap, HashSet};
+use gpui::{AnyElement, ViewContext};
+use language::{BufferSnapshot, Chunk, Patch, Point};
+use parking_lot::Mutex;
+use std::{
+    cell::RefCell,
+    cmp::{self, Ordering},
+    fmt::Debug,
+    ops::{Deref, DerefMut, Range},
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
+};
+use sum_tree::{Bias, SumTree};
+use text::Edit;
+
+const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
+
+pub struct BlockMap {
+    next_block_id: AtomicUsize,
+    wrap_snapshot: RefCell<WrapSnapshot>,
+    blocks: Vec<Arc<Block>>,
+    transforms: RefCell<SumTree<Transform>>,
+    buffer_header_height: u8,
+    excerpt_header_height: u8,
+}
+
+pub struct BlockMapWriter<'a>(&'a mut BlockMap);
+
+pub struct BlockSnapshot {
+    wrap_snapshot: WrapSnapshot,
+    transforms: SumTree<Transform>,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct BlockId(usize);
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct BlockPoint(pub Point);
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+struct BlockRow(u32);
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+struct WrapRow(u32);
+
+pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>;
+
+pub struct Block {
+    id: BlockId,
+    position: Anchor,
+    height: u8,
+    style: BlockStyle,
+    render: Mutex<RenderBlock>,
+    disposition: BlockDisposition,
+}
+
+#[derive(Clone)]
+pub struct BlockProperties<P>
+where
+    P: Clone,
+{
+    pub position: P,
+    pub height: u8,
+    pub style: BlockStyle,
+    pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>,
+    pub disposition: BlockDisposition,
+}
+
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub enum BlockStyle {
+    Fixed,
+    Flex,
+    Sticky,
+}
+
+pub struct BlockContext<'a, 'b, 'c> {
+    pub view_context: &'c mut ViewContext<'a, 'b, Editor>,
+    pub anchor_x: f32,
+    pub scroll_x: f32,
+    pub gutter_width: f32,
+    pub gutter_padding: f32,
+    pub em_width: f32,
+    pub line_height: f32,
+    pub block_id: usize,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum BlockDisposition {
+    Above,
+    Below,
+}
+
+#[derive(Clone, Debug)]
+struct Transform {
+    summary: TransformSummary,
+    block: Option<TransformBlock>,
+}
+
+#[allow(clippy::large_enum_variant)]
+#[derive(Clone)]
+pub enum TransformBlock {
+    Custom(Arc<Block>),
+    ExcerptHeader {
+        id: ExcerptId,
+        buffer: BufferSnapshot,
+        range: ExcerptRange<text::Anchor>,
+        height: u8,
+        starts_new_buffer: bool,
+    },
+}
+
+impl TransformBlock {
+    fn disposition(&self) -> BlockDisposition {
+        match self {
+            TransformBlock::Custom(block) => block.disposition,
+            TransformBlock::ExcerptHeader { .. } => BlockDisposition::Above,
+        }
+    }
+
+    pub fn height(&self) -> u8 {
+        match self {
+            TransformBlock::Custom(block) => block.height,
+            TransformBlock::ExcerptHeader { height, .. } => *height,
+        }
+    }
+}
+
+impl Debug for TransformBlock {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(),
+            Self::ExcerptHeader { buffer, .. } => f
+                .debug_struct("ExcerptHeader")
+                .field("path", &buffer.file().map(|f| f.path()))
+                .finish(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+struct TransformSummary {
+    input_rows: u32,
+    output_rows: u32,
+}
+
+pub struct BlockChunks<'a> {
+    transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
+    input_chunks: wrap_map::WrapChunks<'a>,
+    input_chunk: Chunk<'a>,
+    output_row: u32,
+    max_output_row: u32,
+}
+
+#[derive(Clone)]
+pub struct BlockBufferRows<'a> {
+    transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
+    input_buffer_rows: wrap_map::WrapBufferRows<'a>,
+    output_row: u32,
+    started: bool,
+}
+
+impl BlockMap {
+    pub fn new(
+        wrap_snapshot: WrapSnapshot,
+        buffer_header_height: u8,
+        excerpt_header_height: u8,
+    ) -> Self {
+        let row_count = wrap_snapshot.max_point().row() + 1;
+        let map = Self {
+            next_block_id: AtomicUsize::new(0),
+            blocks: Vec::new(),
+            transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())),
+            wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
+            buffer_header_height,
+            excerpt_header_height,
+        };
+        map.sync(
+            &wrap_snapshot,
+            Patch::new(vec![Edit {
+                old: 0..row_count,
+                new: 0..row_count,
+            }]),
+        );
+        map
+    }
+
+    pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockSnapshot {
+        self.sync(&wrap_snapshot, edits);
+        *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
+        BlockSnapshot {
+            wrap_snapshot,
+            transforms: self.transforms.borrow().clone(),
+        }
+    }
+
+    pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter {
+        self.sync(&wrap_snapshot, edits);
+        *self.wrap_snapshot.borrow_mut() = wrap_snapshot;
+        BlockMapWriter(self)
+    }
+
+    fn sync(&self, wrap_snapshot: &WrapSnapshot, mut edits: Patch<u32>) {
+        let buffer = wrap_snapshot.buffer_snapshot();
+
+        // Handle changing the last excerpt if it is empty.
+        if buffer.trailing_excerpt_update_count()
+            != self
+                .wrap_snapshot
+                .borrow()
+                .buffer_snapshot()
+                .trailing_excerpt_update_count()
+        {
+            let max_point = wrap_snapshot.max_point();
+            let edit_start = wrap_snapshot.prev_row_boundary(max_point);
+            let edit_end = max_point.row() + 1;
+            edits = edits.compose([WrapEdit {
+                old: edit_start..edit_end,
+                new: edit_start..edit_end,
+            }]);
+        }
+
+        let edits = edits.into_inner();
+        if edits.is_empty() {
+            return;
+        }
+
+        let mut transforms = self.transforms.borrow_mut();
+        let mut new_transforms = SumTree::new();
+        let old_row_count = transforms.summary().input_rows;
+        let new_row_count = wrap_snapshot.max_point().row() + 1;
+        let mut cursor = transforms.cursor::<WrapRow>();
+        let mut last_block_ix = 0;
+        let mut blocks_in_edit = Vec::new();
+        let mut edits = edits.into_iter().peekable();
+
+        while let Some(edit) = edits.next() {
+            // Preserve any old transforms that precede this edit.
+            let old_start = WrapRow(edit.old.start);
+            let new_start = WrapRow(edit.new.start);
+            new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &());
+            if let Some(transform) = cursor.item() {
+                if transform.is_isomorphic() && old_start == cursor.end(&()) {
+                    new_transforms.push(transform.clone(), &());
+                    cursor.next(&());
+                    while let Some(transform) = cursor.item() {
+                        if transform
+                            .block
+                            .as_ref()
+                            .map_or(false, |b| b.disposition().is_below())
+                        {
+                            new_transforms.push(transform.clone(), &());
+                            cursor.next(&());
+                        } else {
+                            break;
+                        }
+                    }
+                }
+            }
+
+            // Preserve any portion of an old transform that precedes this edit.
+            let extent_before_edit = old_start.0 - cursor.start().0;
+            push_isomorphic(&mut new_transforms, extent_before_edit);
+
+            // Skip over any old transforms that intersect this edit.
+            let mut old_end = WrapRow(edit.old.end);
+            let mut new_end = WrapRow(edit.new.end);
+            cursor.seek(&old_end, Bias::Left, &());
+            cursor.next(&());
+            if old_end == *cursor.start() {
+                while let Some(transform) = cursor.item() {
+                    if transform
+                        .block
+                        .as_ref()
+                        .map_or(false, |b| b.disposition().is_below())
+                    {
+                        cursor.next(&());
+                    } else {
+                        break;
+                    }
+                }
+            }
+
+            // Combine this edit with any subsequent edits that intersect the same transform.
+            while let Some(next_edit) = edits.peek() {
+                if next_edit.old.start <= cursor.start().0 {
+                    old_end = WrapRow(next_edit.old.end);
+                    new_end = WrapRow(next_edit.new.end);
+                    cursor.seek(&old_end, Bias::Left, &());
+                    cursor.next(&());
+                    if old_end == *cursor.start() {
+                        while let Some(transform) = cursor.item() {
+                            if transform
+                                .block
+                                .as_ref()
+                                .map_or(false, |b| b.disposition().is_below())
+                            {
+                                cursor.next(&());
+                            } else {
+                                break;
+                            }
+                        }
+                    }
+                    edits.next();
+                } else {
+                    break;
+                }
+            }
+
+            // Find the blocks within this edited region.
+            let new_buffer_start =
+                wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left);
+            let start_bound = Bound::Included(new_buffer_start);
+            let start_block_ix = match self.blocks[last_block_ix..].binary_search_by(|probe| {
+                probe
+                    .position
+                    .to_point(buffer)
+                    .cmp(&new_buffer_start)
+                    .then(Ordering::Greater)
+            }) {
+                Ok(ix) | Err(ix) => last_block_ix + ix,
+            };
+
+            let end_bound;
+            let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() {
+                end_bound = Bound::Unbounded;
+                self.blocks.len()
+            } else {
+                let new_buffer_end =
+                    wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left);
+                end_bound = Bound::Excluded(new_buffer_end);
+                match self.blocks[start_block_ix..].binary_search_by(|probe| {
+                    probe
+                        .position
+                        .to_point(buffer)
+                        .cmp(&new_buffer_end)
+                        .then(Ordering::Greater)
+                }) {
+                    Ok(ix) | Err(ix) => start_block_ix + ix,
+                }
+            };
+            last_block_ix = end_block_ix;
+
+            debug_assert!(blocks_in_edit.is_empty());
+            blocks_in_edit.extend(
+                self.blocks[start_block_ix..end_block_ix]
+                    .iter()
+                    .map(|block| {
+                        let mut position = block.position.to_point(buffer);
+                        match block.disposition {
+                            BlockDisposition::Above => position.column = 0,
+                            BlockDisposition::Below => {
+                                position.column = buffer.line_len(position.row)
+                            }
+                        }
+                        let position = wrap_snapshot.make_wrap_point(position, Bias::Left);
+                        (position.row(), TransformBlock::Custom(block.clone()))
+                    }),
+            );
+            blocks_in_edit.extend(
+                buffer
+                    .excerpt_boundaries_in_range((start_bound, end_bound))
+                    .map(|excerpt_boundary| {
+                        (
+                            wrap_snapshot
+                                .make_wrap_point(Point::new(excerpt_boundary.row, 0), Bias::Left)
+                                .row(),
+                            TransformBlock::ExcerptHeader {
+                                id: excerpt_boundary.id,
+                                buffer: excerpt_boundary.buffer,
+                                range: excerpt_boundary.range,
+                                height: if excerpt_boundary.starts_new_buffer {
+                                    self.buffer_header_height
+                                } else {
+                                    self.excerpt_header_height
+                                },
+                                starts_new_buffer: excerpt_boundary.starts_new_buffer,
+                            },
+                        )
+                    }),
+            );
+
+            // Place excerpt headers above custom blocks on the same row.
+            blocks_in_edit.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| {
+                row_a.cmp(row_b).then_with(|| match (block_a, block_b) {
+                    (
+                        TransformBlock::ExcerptHeader { .. },
+                        TransformBlock::ExcerptHeader { .. },
+                    ) => Ordering::Equal,
+                    (TransformBlock::ExcerptHeader { .. }, _) => Ordering::Less,
+                    (_, TransformBlock::ExcerptHeader { .. }) => Ordering::Greater,
+                    (TransformBlock::Custom(block_a), TransformBlock::Custom(block_b)) => block_a
+                        .disposition
+                        .cmp(&block_b.disposition)
+                        .then_with(|| block_a.id.cmp(&block_b.id)),
+                })
+            });
+
+            // For each of these blocks, insert a new isomorphic transform preceding the block,
+            // and then insert the block itself.
+            for (block_row, block) in blocks_in_edit.drain(..) {
+                let insertion_row = match block.disposition() {
+                    BlockDisposition::Above => block_row,
+                    BlockDisposition::Below => block_row + 1,
+                };
+                let extent_before_block = insertion_row - new_transforms.summary().input_rows;
+                push_isomorphic(&mut new_transforms, extent_before_block);
+                new_transforms.push(Transform::block(block), &());
+            }
+
+            old_end = WrapRow(old_end.0.min(old_row_count));
+            new_end = WrapRow(new_end.0.min(new_row_count));
+
+            // Insert an isomorphic transform after the final block.
+            let extent_after_last_block = new_end.0 - new_transforms.summary().input_rows;
+            push_isomorphic(&mut new_transforms, extent_after_last_block);
+
+            // Preserve any portion of the old transform after this edit.
+            let extent_after_edit = cursor.start().0 - old_end.0;
+            push_isomorphic(&mut new_transforms, extent_after_edit);
+        }
+
+        new_transforms.append(cursor.suffix(&()), &());
+        debug_assert_eq!(
+            new_transforms.summary().input_rows,
+            wrap_snapshot.max_point().row() + 1
+        );
+
+        drop(cursor);
+        *transforms = new_transforms;
+    }
+
+    pub fn replace(&mut self, mut renderers: HashMap<BlockId, RenderBlock>) {
+        for block in &self.blocks {
+            if let Some(render) = renderers.remove(&block.id) {
+                *block.render.lock() = render;
+            }
+        }
+    }
+}
+
+fn push_isomorphic(tree: &mut SumTree<Transform>, rows: u32) {
+    if rows == 0 {
+        return;
+    }
+
+    let mut extent = Some(rows);
+    tree.update_last(
+        |last_transform| {
+            if last_transform.is_isomorphic() {
+                let extent = extent.take().unwrap();
+                last_transform.summary.input_rows += extent;
+                last_transform.summary.output_rows += extent;
+            }
+        },
+        &(),
+    );
+    if let Some(extent) = extent {
+        tree.push(Transform::isomorphic(extent), &());
+    }
+}
+
+impl BlockPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(Point::new(row, column))
+    }
+}
+
+impl Deref for BlockPoint {
+    type Target = Point;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl std::ops::DerefMut for BlockPoint {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl<'a> BlockMapWriter<'a> {
+    pub fn insert(
+        &mut self,
+        blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
+    ) -> Vec<BlockId> {
+        let mut ids = Vec::new();
+        let mut edits = Patch::default();
+        let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
+        let buffer = wrap_snapshot.buffer_snapshot();
+
+        for block in blocks {
+            let id = BlockId(self.0.next_block_id.fetch_add(1, SeqCst));
+            ids.push(id);
+
+            let position = block.position;
+            let point = position.to_point(buffer);
+            let wrap_row = wrap_snapshot
+                .make_wrap_point(Point::new(point.row, 0), Bias::Left)
+                .row();
+            let start_row = wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0));
+            let end_row = wrap_snapshot
+                .next_row_boundary(WrapPoint::new(wrap_row, 0))
+                .unwrap_or(wrap_snapshot.max_point().row() + 1);
+
+            let block_ix = match self
+                .0
+                .blocks
+                .binary_search_by(|probe| probe.position.cmp(&position, buffer))
+            {
+                Ok(ix) | Err(ix) => ix,
+            };
+            self.0.blocks.insert(
+                block_ix,
+                Arc::new(Block {
+                    id,
+                    position,
+                    height: block.height,
+                    render: Mutex::new(block.render),
+                    disposition: block.disposition,
+                    style: block.style,
+                }),
+            );
+
+            edits = edits.compose([Edit {
+                old: start_row..end_row,
+                new: start_row..end_row,
+            }]);
+        }
+
+        self.0.sync(wrap_snapshot, edits);
+        ids
+    }
+
+    pub fn remove(&mut self, block_ids: HashSet<BlockId>) {
+        let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
+        let buffer = wrap_snapshot.buffer_snapshot();
+        let mut edits = Patch::default();
+        let mut last_block_buffer_row = None;
+        self.0.blocks.retain(|block| {
+            if block_ids.contains(&block.id) {
+                let buffer_row = block.position.to_point(buffer).row;
+                if last_block_buffer_row != Some(buffer_row) {
+                    last_block_buffer_row = Some(buffer_row);
+                    let wrap_row = wrap_snapshot
+                        .make_wrap_point(Point::new(buffer_row, 0), Bias::Left)
+                        .row();
+                    let start_row = wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0));
+                    let end_row = wrap_snapshot
+                        .next_row_boundary(WrapPoint::new(wrap_row, 0))
+                        .unwrap_or(wrap_snapshot.max_point().row() + 1);
+                    edits.push(Edit {
+                        old: start_row..end_row,
+                        new: start_row..end_row,
+                    })
+                }
+                false
+            } else {
+                true
+            }
+        });
+        self.0.sync(wrap_snapshot, edits);
+    }
+}
+
+impl BlockSnapshot {
+    #[cfg(test)]
+    pub fn text(&self) -> String {
+        self.chunks(
+            0..self.transforms.summary().output_rows,
+            false,
+            Highlights::default(),
+        )
+        .map(|chunk| chunk.text)
+        .collect()
+    }
+
+    pub fn chunks<'a>(
+        &'a self,
+        rows: Range<u32>,
+        language_aware: bool,
+        highlights: Highlights<'a>,
+    ) -> BlockChunks<'a> {
+        let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        let input_end = {
+            cursor.seek(&BlockRow(rows.end), Bias::Right, &());
+            let overshoot = if cursor
+                .item()
+                .map_or(false, |transform| transform.is_isomorphic())
+            {
+                rows.end - cursor.start().0 .0
+            } else {
+                0
+            };
+            cursor.start().1 .0 + overshoot
+        };
+        let input_start = {
+            cursor.seek(&BlockRow(rows.start), Bias::Right, &());
+            let overshoot = if cursor
+                .item()
+                .map_or(false, |transform| transform.is_isomorphic())
+            {
+                rows.start - cursor.start().0 .0
+            } else {
+                0
+            };
+            cursor.start().1 .0 + overshoot
+        };
+        BlockChunks {
+            input_chunks: self.wrap_snapshot.chunks(
+                input_start..input_end,
+                language_aware,
+                highlights,
+            ),
+            input_chunk: Default::default(),
+            transforms: cursor,
+            output_row: rows.start,
+            max_output_row,
+        }
+    }
+
+    pub fn buffer_rows(&self, start_row: u32) -> BlockBufferRows {
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        cursor.seek(&BlockRow(start_row), Bias::Right, &());
+        let (output_start, input_start) = cursor.start();
+        let overshoot = if cursor.item().map_or(false, |t| t.is_isomorphic()) {
+            start_row - output_start.0
+        } else {
+            0
+        };
+        let input_start_row = input_start.0 + overshoot;
+        BlockBufferRows {
+            transforms: cursor,
+            input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row),
+            output_row: start_row,
+            started: false,
+        }
+    }
+
+    pub fn blocks_in_range(
+        &self,
+        rows: Range<u32>,
+    ) -> impl Iterator<Item = (u32, &TransformBlock)> {
+        let mut cursor = self.transforms.cursor::<BlockRow>();
+        cursor.seek(&BlockRow(rows.start), Bias::Right, &());
+        std::iter::from_fn(move || {
+            while let Some(transform) = cursor.item() {
+                let start_row = cursor.start().0;
+                if start_row >= rows.end {
+                    break;
+                }
+                if let Some(block) = &transform.block {
+                    cursor.next(&());
+                    return Some((start_row, block));
+                } else {
+                    cursor.next(&());
+                }
+            }
+            None
+        })
+    }
+
+    pub fn max_point(&self) -> BlockPoint {
+        let row = self.transforms.summary().output_rows - 1;
+        BlockPoint::new(row, self.line_len(row))
+    }
+
+    pub fn longest_row(&self) -> u32 {
+        let input_row = self.wrap_snapshot.longest_row();
+        self.to_block_point(WrapPoint::new(input_row, 0)).row
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        cursor.seek(&BlockRow(row), Bias::Right, &());
+        if let Some(transform) = cursor.item() {
+            let (output_start, input_start) = cursor.start();
+            let overshoot = row - output_start.0;
+            if transform.block.is_some() {
+                0
+            } else {
+                self.wrap_snapshot.line_len(input_start.0 + overshoot)
+            }
+        } else {
+            panic!("row out of range");
+        }
+    }
+
+    pub fn is_block_line(&self, row: u32) -> bool {
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        cursor.seek(&BlockRow(row), Bias::Right, &());
+        cursor.item().map_or(false, |t| t.block.is_some())
+    }
+
+    pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint {
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        cursor.seek(&BlockRow(point.row), Bias::Right, &());
+
+        let max_input_row = WrapRow(self.transforms.summary().input_rows);
+        let mut search_left =
+            (bias == Bias::Left && cursor.start().1 .0 > 0) || cursor.end(&()).1 == max_input_row;
+        let mut reversed = false;
+
+        loop {
+            if let Some(transform) = cursor.item() {
+                if transform.is_isomorphic() {
+                    let (output_start_row, input_start_row) = cursor.start();
+                    let (output_end_row, input_end_row) = cursor.end(&());
+                    let output_start = Point::new(output_start_row.0, 0);
+                    let input_start = Point::new(input_start_row.0, 0);
+                    let input_end = Point::new(input_end_row.0, 0);
+                    let input_point = if point.row >= output_end_row.0 {
+                        let line_len = self.wrap_snapshot.line_len(input_end_row.0 - 1);
+                        self.wrap_snapshot
+                            .clip_point(WrapPoint::new(input_end_row.0 - 1, line_len), bias)
+                    } else {
+                        let output_overshoot = point.0.saturating_sub(output_start);
+                        self.wrap_snapshot
+                            .clip_point(WrapPoint(input_start + output_overshoot), bias)
+                    };
+
+                    if (input_start..input_end).contains(&input_point.0) {
+                        let input_overshoot = input_point.0.saturating_sub(input_start);
+                        return BlockPoint(output_start + input_overshoot);
+                    }
+                }
+
+                if search_left {
+                    cursor.prev(&());
+                } else {
+                    cursor.next(&());
+                }
+            } else if reversed {
+                return self.max_point();
+            } else {
+                reversed = true;
+                search_left = !search_left;
+                cursor.seek(&BlockRow(point.row), Bias::Right, &());
+            }
+        }
+    }
+
+    pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint {
+        let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>();
+        cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
+        if let Some(transform) = cursor.item() {
+            debug_assert!(transform.is_isomorphic());
+        } else {
+            return self.max_point();
+        }
+
+        let (input_start_row, output_start_row) = cursor.start();
+        let input_start = Point::new(input_start_row.0, 0);
+        let output_start = Point::new(output_start_row.0, 0);
+        let input_overshoot = wrap_point.0 - input_start;
+        BlockPoint(output_start + input_overshoot)
+    }
+
+    pub fn to_wrap_point(&self, block_point: BlockPoint) -> WrapPoint {
+        let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+        cursor.seek(&BlockRow(block_point.row), Bias::Right, &());
+        if let Some(transform) = cursor.item() {
+            match transform.block.as_ref().map(|b| b.disposition()) {
+                Some(BlockDisposition::Above) => WrapPoint::new(cursor.start().1 .0, 0),
+                Some(BlockDisposition::Below) => {
+                    let wrap_row = cursor.start().1 .0 - 1;
+                    WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row))
+                }
+                None => {
+                    let overshoot = block_point.row - cursor.start().0 .0;
+                    let wrap_row = cursor.start().1 .0 + overshoot;
+                    WrapPoint::new(wrap_row, block_point.column)
+                }
+            }
+        } else {
+            self.wrap_snapshot.max_point()
+        }
+    }
+}
+
+impl Transform {
+    fn isomorphic(rows: u32) -> Self {
+        Self {
+            summary: TransformSummary {
+                input_rows: rows,
+                output_rows: rows,
+            },
+            block: None,
+        }
+    }
+
+    fn block(block: TransformBlock) -> Self {
+        Self {
+            summary: TransformSummary {
+                input_rows: 0,
+                output_rows: block.height() as u32,
+            },
+            block: Some(block),
+        }
+    }
+
+    fn is_isomorphic(&self) -> bool {
+        self.block.is_none()
+    }
+}
+
+impl<'a> Iterator for BlockChunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.output_row >= self.max_output_row {
+            return None;
+        }
+
+        let transform = self.transforms.item()?;
+        if transform.block.is_some() {
+            let block_start = self.transforms.start().0 .0;
+            let mut block_end = self.transforms.end(&()).0 .0;
+            self.transforms.next(&());
+            if self.transforms.item().is_none() {
+                block_end -= 1;
+            }
+
+            let start_in_block = self.output_row - block_start;
+            let end_in_block = cmp::min(self.max_output_row, block_end) - block_start;
+            let line_count = end_in_block - start_in_block;
+            self.output_row += line_count;
+
+            return Some(Chunk {
+                text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
+                ..Default::default()
+            });
+        }
+
+        if self.input_chunk.text.is_empty() {
+            if let Some(input_chunk) = self.input_chunks.next() {
+                self.input_chunk = input_chunk;
+            } else {
+                self.output_row += 1;
+                if self.output_row < self.max_output_row {
+                    self.transforms.next(&());
+                    return Some(Chunk {
+                        text: "\n",
+                        ..Default::default()
+                    });
+                } else {
+                    return None;
+                }
+            }
+        }
+
+        let transform_end = self.transforms.end(&()).0 .0;
+        let (prefix_rows, prefix_bytes) =
+            offset_for_row(self.input_chunk.text, transform_end - self.output_row);
+        self.output_row += prefix_rows;
+        let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
+        self.input_chunk.text = suffix;
+        if self.output_row == transform_end {
+            self.transforms.next(&());
+        }
+
+        Some(Chunk {
+            text: prefix,
+            ..self.input_chunk
+        })
+    }
+}
+
+impl<'a> Iterator for BlockBufferRows<'a> {
+    type Item = Option<u32>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.started {
+            self.output_row += 1;
+        } else {
+            self.started = true;
+        }
+
+        if self.output_row >= self.transforms.end(&()).0 .0 {
+            self.transforms.next(&());
+        }
+
+        let transform = self.transforms.item()?;
+        if transform.block.is_some() {
+            Some(None)
+        } else {
+            Some(self.input_buffer_rows.next().unwrap())
+        }
+    }
+}
+
+impl sum_tree::Item for Transform {
+    type Summary = TransformSummary;
+
+    fn summary(&self) -> Self::Summary {
+        self.summary.clone()
+    }
+}
+
+impl sum_tree::Summary for TransformSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, summary: &Self, _: &()) {
+        self.input_rows += summary.input_rows;
+        self.output_rows += summary.output_rows;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapRow {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += summary.input_rows;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for BlockRow {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += summary.output_rows;
+    }
+}
+
+impl BlockDisposition {
+    fn is_below(&self) -> bool {
+        matches!(self, BlockDisposition::Below)
+    }
+}
+
+impl<'a, 'b, 'c> Deref for BlockContext<'a, 'b, 'c> {
+    type Target = ViewContext<'a, 'b, Editor>;
+
+    fn deref(&self) -> &Self::Target {
+        self.view_context
+    }
+}
+
+impl DerefMut for BlockContext<'_, '_, '_> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.view_context
+    }
+}
+
+impl Block {
+    pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> {
+        self.render.lock()(cx)
+    }
+
+    pub fn position(&self) -> &Anchor {
+        &self.position
+    }
+
+    pub fn style(&self) -> BlockStyle {
+        self.style
+    }
+}
+
+impl Debug for Block {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Block")
+            .field("id", &self.id)
+            .field("position", &self.position)
+            .field("disposition", &self.disposition)
+            .finish()
+    }
+}
+
+// Count the number of bytes prior to a target point. If the string doesn't contain the target
+// point, return its total extent. Otherwise return the target point itself.
+fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
+    let mut row = 0;
+    let mut offset = 0;
+    for (ix, line) in s.split('\n').enumerate() {
+        if ix > 0 {
+            row += 1;
+            offset += 1;
+        }
+        if row >= target {
+            break;
+        }
+        offset += line.len() as usize;
+    }
+    (row, offset)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::display_map::inlay_map::InlayMap;
+    use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
+    use gpui::{elements::Empty, Element};
+    use multi_buffer::MultiBuffer;
+    use rand::prelude::*;
+    use settings::SettingsStore;
+    use std::env;
+    use util::RandomCharIter;
+
+    #[gpui::test]
+    fn test_offset_for_row() {
+        assert_eq!(offset_for_row("", 0), (0, 0));
+        assert_eq!(offset_for_row("", 1), (0, 0));
+        assert_eq!(offset_for_row("abcd", 0), (0, 0));
+        assert_eq!(offset_for_row("abcd", 1), (0, 4));
+        assert_eq!(offset_for_row("\n", 0), (0, 0));
+        assert_eq!(offset_for_row("\n", 1), (1, 1));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11));
+    }
+
+    #[gpui::test]
+    fn test_basic_blocks(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+
+        let text = "aaa\nbbb\nccc\nddd";
+
+        let buffer = MultiBuffer::build_simple(text, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
+        let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
+        let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
+
+        let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
+        let block_ids = writer.insert(vec![
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(1, 0)),
+                height: 1,
+                disposition: BlockDisposition::Above,
+                render: Arc::new(|_| Empty::new().into_any_named("block 1")),
+            },
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(1, 2)),
+                height: 2,
+                disposition: BlockDisposition::Above,
+                render: Arc::new(|_| Empty::new().into_any_named("block 2")),
+            },
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(3, 3)),
+                height: 3,
+                disposition: BlockDisposition::Below,
+                render: Arc::new(|_| Empty::new().into_any_named("block 3")),
+            },
+        ]);
+
+        let snapshot = block_map.read(wraps_snapshot, Default::default());
+        assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
+
+        let blocks = snapshot
+            .blocks_in_range(0..8)
+            .map(|(start_row, block)| {
+                let block = block.as_custom().unwrap();
+                (start_row..start_row + block.height as u32, block.id)
+            })
+            .collect::<Vec<_>>();
+
+        // When multiple blocks are on the same line, the newer blocks appear first.
+        assert_eq!(
+            blocks,
+            &[
+                (1..2, block_ids[0]),
+                (2..4, block_ids[1]),
+                (7..10, block_ids[2]),
+            ]
+        );
+
+        assert_eq!(
+            snapshot.to_block_point(WrapPoint::new(0, 3)),
+            BlockPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.to_block_point(WrapPoint::new(1, 0)),
+            BlockPoint::new(4, 0)
+        );
+        assert_eq!(
+            snapshot.to_block_point(WrapPoint::new(3, 3)),
+            BlockPoint::new(6, 3)
+        );
+
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(0, 3)),
+            WrapPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(1, 0)),
+            WrapPoint::new(1, 0)
+        );
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(3, 0)),
+            WrapPoint::new(1, 0)
+        );
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(7, 0)),
+            WrapPoint::new(3, 3)
+        );
+
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left),
+            BlockPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right),
+            BlockPoint::new(4, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left),
+            BlockPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right),
+            BlockPoint::new(4, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(4, 0), Bias::Left),
+            BlockPoint::new(4, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(4, 0), Bias::Right),
+            BlockPoint::new(4, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(6, 3), Bias::Left),
+            BlockPoint::new(6, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(6, 3), Bias::Right),
+            BlockPoint::new(6, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(7, 0), Bias::Left),
+            BlockPoint::new(6, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(7, 0), Bias::Right),
+            BlockPoint::new(6, 3)
+        );
+
+        assert_eq!(
+            snapshot.buffer_rows(0).collect::<Vec<_>>(),
+            &[
+                Some(0),
+                None,
+                None,
+                None,
+                Some(1),
+                Some(2),
+                Some(3),
+                None,
+                None,
+                None
+            ]
+        );
+
+        // Insert a line break, separating two block decorations into separate lines.
+        let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx);
+            buffer.snapshot(cx)
+        });
+
+        let (inlay_snapshot, inlay_edits) =
+            inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+        let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+        let (tab_snapshot, tab_edits) =
+            tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
+        let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+            wrap_map.sync(tab_snapshot, tab_edits, cx)
+        });
+        let snapshot = block_map.read(wraps_snapshot, wrap_edits);
+        assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
+    }
+
+    #[gpui::test]
+    fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+
+        let text = "one two three\nfour five six\nseven eight";
+
+        let buffer = MultiBuffer::build_simple(text, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
+        let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
+
+        let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
+        writer.insert(vec![
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(1, 12)),
+                disposition: BlockDisposition::Above,
+                render: Arc::new(|_| Empty::new().into_any_named("block 1")),
+                height: 1,
+            },
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(1, 1)),
+                disposition: BlockDisposition::Below,
+                render: Arc::new(|_| Empty::new().into_any_named("block 2")),
+                height: 1,
+            },
+        ]);
+
+        // Blocks with an 'above' disposition go above their corresponding buffer line.
+        // Blocks with a 'below' disposition go below their corresponding buffer line.
+        let snapshot = block_map.read(wraps_snapshot, Default::default());
+        assert_eq!(
+            snapshot.text(),
+            "one two \nthree\n\nfour five \nsix\n\nseven \neight"
+        );
+    }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) {
+        init_test(cx);
+
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let wrap_width = if rng.gen_bool(0.2) {
+            None
+        } else {
+            Some(rng.gen_range(0.0..=100.0))
+        };
+        let tab_size = 1.try_into().unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let buffer_start_header_height = rng.gen_range(1..=5);
+        let excerpt_header_height = rng.gen_range(1..=5);
+
+        log::info!("Wrap width: {:?}", wrap_width);
+        log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
+
+        let buffer = if rng.gen() {
+            let len = rng.gen_range(0..10);
+            let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+            log::info!("initial buffer text: {:?}", text);
+            MultiBuffer::build_simple(&text, cx)
+        } else {
+            MultiBuffer::build_random(&mut rng, cx)
+        };
+
+        let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (wrap_map, wraps_snapshot) =
+            WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
+        let mut block_map = BlockMap::new(
+            wraps_snapshot,
+            buffer_start_header_height,
+            excerpt_header_height,
+        );
+        let mut custom_blocks = Vec::new();
+
+        for _ in 0..operations {
+            let mut buffer_edits = Vec::new();
+            match rng.gen_range(0..=100) {
+                0..=19 => {
+                    let wrap_width = if rng.gen_bool(0.2) {
+                        None
+                    } else {
+                        Some(rng.gen_range(0.0..=100.0))
+                    };
+                    log::info!("Setting wrap width to {:?}", wrap_width);
+                    wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+                }
+                20..=39 => {
+                    let block_count = rng.gen_range(1..=5);
+                    let block_properties = (0..block_count)
+                        .map(|_| {
+                            let buffer = buffer.read(cx).read(cx);
+                            let position = buffer.anchor_after(
+                                buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left),
+                            );
+
+                            let disposition = if rng.gen() {
+                                BlockDisposition::Above
+                            } else {
+                                BlockDisposition::Below
+                            };
+                            let height = rng.gen_range(1..5);
+                            log::info!(
+                                "inserting block {:?} {:?} with height {}",
+                                disposition,
+                                position.to_point(&buffer),
+                                height
+                            );
+                            BlockProperties {
+                                style: BlockStyle::Fixed,
+                                position,
+                                height,
+                                disposition,
+                                render: Arc::new(|_| Empty::new().into_any()),
+                            }
+                        })
+                        .collect::<Vec<_>>();
+
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.sync(buffer_snapshot.clone(), vec![]);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+                    let (tab_snapshot, tab_edits) =
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                    let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+                        wrap_map.sync(tab_snapshot, tab_edits, cx)
+                    });
+                    let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
+                    let block_ids = block_map.insert(block_properties.clone());
+                    for (block_id, props) in block_ids.into_iter().zip(block_properties) {
+                        custom_blocks.push((block_id, props));
+                    }
+                }
+                40..=59 if !custom_blocks.is_empty() => {
+                    let block_count = rng.gen_range(1..=4.min(custom_blocks.len()));
+                    let block_ids_to_remove = (0..block_count)
+                        .map(|_| {
+                            custom_blocks
+                                .remove(rng.gen_range(0..custom_blocks.len()))
+                                .0
+                        })
+                        .collect();
+
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.sync(buffer_snapshot.clone(), vec![]);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+                    let (tab_snapshot, tab_edits) =
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                    let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+                        wrap_map.sync(tab_snapshot, tab_edits, cx)
+                    });
+                    let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
+                    block_map.remove(block_ids_to_remove);
+                }
+                _ => {
+                    buffer.update(cx, |buffer, cx| {
+                        let mutation_count = rng.gen_range(1..=5);
+                        let subscription = buffer.subscribe();
+                        buffer.randomly_mutate(&mut rng, mutation_count, cx);
+                        buffer_snapshot = buffer.snapshot(cx);
+                        buffer_edits.extend(subscription.consume());
+                        log::info!("buffer text: {:?}", buffer_snapshot.text());
+                    });
+                }
+            }
+
+            let (inlay_snapshot, inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+            let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
+            let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+                wrap_map.sync(tab_snapshot, tab_edits, cx)
+            });
+            let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
+            assert_eq!(
+                blocks_snapshot.transforms.summary().input_rows,
+                wraps_snapshot.max_point().row() + 1
+            );
+            log::info!("blocks text: {:?}", blocks_snapshot.text());
+
+            let mut expected_blocks = Vec::new();
+            expected_blocks.extend(custom_blocks.iter().map(|(id, block)| {
+                let mut position = block.position.to_point(&buffer_snapshot);
+                match block.disposition {
+                    BlockDisposition::Above => {
+                        position.column = 0;
+                    }
+                    BlockDisposition::Below => {
+                        position.column = buffer_snapshot.line_len(position.row);
+                    }
+                };
+                let row = wraps_snapshot.make_wrap_point(position, Bias::Left).row();
+                (
+                    row,
+                    ExpectedBlock::Custom {
+                        disposition: block.disposition,
+                        id: *id,
+                        height: block.height,
+                    },
+                )
+            }));
+            expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map(
+                |boundary| {
+                    let position =
+                        wraps_snapshot.make_wrap_point(Point::new(boundary.row, 0), Bias::Left);
+                    (
+                        position.row(),
+                        ExpectedBlock::ExcerptHeader {
+                            height: if boundary.starts_new_buffer {
+                                buffer_start_header_height
+                            } else {
+                                excerpt_header_height
+                            },
+                            starts_new_buffer: boundary.starts_new_buffer,
+                        },
+                    )
+                },
+            ));
+            expected_blocks.sort_unstable();
+            let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
+
+            let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::<Vec<_>>();
+            let mut expected_buffer_rows = Vec::new();
+            let mut expected_text = String::new();
+            let mut expected_block_positions = Vec::new();
+            let input_text = wraps_snapshot.text();
+            for (row, input_line) in input_text.split('\n').enumerate() {
+                let row = row as u32;
+                if row > 0 {
+                    expected_text.push('\n');
+                }
+
+                let buffer_row = input_buffer_rows[wraps_snapshot
+                    .to_point(WrapPoint::new(row, 0), Bias::Left)
+                    .row as usize];
+
+                while let Some((block_row, block)) = sorted_blocks_iter.peek() {
+                    if *block_row == row && block.disposition() == BlockDisposition::Above {
+                        let (_, block) = sorted_blocks_iter.next().unwrap();
+                        let height = block.height() as usize;
+                        expected_block_positions
+                            .push((expected_text.matches('\n').count() as u32, block));
+                        let text = "\n".repeat(height);
+                        expected_text.push_str(&text);
+                        for _ in 0..height {
+                            expected_buffer_rows.push(None);
+                        }
+                    } else {
+                        break;
+                    }
+                }
+
+                let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0;
+                expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });
+                expected_text.push_str(input_line);
+
+                while let Some((block_row, block)) = sorted_blocks_iter.peek() {
+                    if *block_row == row && block.disposition() == BlockDisposition::Below {
+                        let (_, block) = sorted_blocks_iter.next().unwrap();
+                        let height = block.height() as usize;
+                        expected_block_positions
+                            .push((expected_text.matches('\n').count() as u32 + 1, block));
+                        let text = "\n".repeat(height);
+                        expected_text.push_str(&text);
+                        for _ in 0..height {
+                            expected_buffer_rows.push(None);
+                        }
+                    } else {
+                        break;
+                    }
+                }
+            }
+
+            let expected_lines = expected_text.split('\n').collect::<Vec<_>>();
+            let expected_row_count = expected_lines.len();
+            for start_row in 0..expected_row_count {
+                let expected_text = expected_lines[start_row..].join("\n");
+                let actual_text = blocks_snapshot
+                    .chunks(
+                        start_row as u32..blocks_snapshot.max_point().row + 1,
+                        false,
+                        Highlights::default(),
+                    )
+                    .map(|chunk| chunk.text)
+                    .collect::<String>();
+                assert_eq!(
+                    actual_text, expected_text,
+                    "incorrect text starting from row {}",
+                    start_row
+                );
+                assert_eq!(
+                    blocks_snapshot
+                        .buffer_rows(start_row as u32)
+                        .collect::<Vec<_>>(),
+                    &expected_buffer_rows[start_row..]
+                );
+            }
+
+            assert_eq!(
+                blocks_snapshot
+                    .blocks_in_range(0..(expected_row_count as u32))
+                    .map(|(row, block)| (row, block.clone().into()))
+                    .collect::<Vec<_>>(),
+                expected_block_positions
+            );
+
+            let mut expected_longest_rows = Vec::new();
+            let mut longest_line_len = -1_isize;
+            for (row, line) in expected_lines.iter().enumerate() {
+                let row = row as u32;
+
+                assert_eq!(
+                    blocks_snapshot.line_len(row),
+                    line.len() as u32,
+                    "invalid line len for row {}",
+                    row
+                );
+
+                let line_char_count = line.chars().count() as isize;
+                match line_char_count.cmp(&longest_line_len) {
+                    Ordering::Less => {}
+                    Ordering::Equal => expected_longest_rows.push(row),
+                    Ordering::Greater => {
+                        longest_line_len = line_char_count;
+                        expected_longest_rows.clear();
+                        expected_longest_rows.push(row);
+                    }
+                }
+            }
+
+            let longest_row = blocks_snapshot.longest_row();
+            assert!(
+                expected_longest_rows.contains(&longest_row),
+                "incorrect longest row {}. expected {:?} with length {}",
+                longest_row,
+                expected_longest_rows,
+                longest_line_len,
+            );
+
+            for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
+                let wrap_point = WrapPoint::new(row, 0);
+                let block_point = blocks_snapshot.to_block_point(wrap_point);
+                assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point);
+            }
+
+            let mut block_point = BlockPoint::new(0, 0);
+            for c in expected_text.chars() {
+                let left_point = blocks_snapshot.clip_point(block_point, Bias::Left);
+                let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left);
+                assert_eq!(
+                    blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)),
+                    left_point
+                );
+                assert_eq!(
+                    left_buffer_point,
+                    buffer_snapshot.clip_point(left_buffer_point, Bias::Right),
+                    "{:?} is not valid in buffer coordinates",
+                    left_point
+                );
+
+                let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
+                let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right);
+                assert_eq!(
+                    blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)),
+                    right_point
+                );
+                assert_eq!(
+                    right_buffer_point,
+                    buffer_snapshot.clip_point(right_buffer_point, Bias::Left),
+                    "{:?} is not valid in buffer coordinates",
+                    right_point
+                );
+
+                if c == '\n' {
+                    block_point.0 += Point::new(1, 0);
+                } else {
+                    block_point.column += c.len_utf8() as u32;
+                }
+            }
+        }
+
+        #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
+        enum ExpectedBlock {
+            ExcerptHeader {
+                height: u8,
+                starts_new_buffer: bool,
+            },
+            Custom {
+                disposition: BlockDisposition,
+                id: BlockId,
+                height: u8,
+            },
+        }
+
+        impl ExpectedBlock {
+            fn height(&self) -> u8 {
+                match self {
+                    ExpectedBlock::ExcerptHeader { height, .. } => *height,
+                    ExpectedBlock::Custom { height, .. } => *height,
+                }
+            }
+
+            fn disposition(&self) -> BlockDisposition {
+                match self {
+                    ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
+                    ExpectedBlock::Custom { disposition, .. } => *disposition,
+                }
+            }
+        }
+
+        impl From<TransformBlock> for ExpectedBlock {
+            fn from(block: TransformBlock) -> Self {
+                match block {
+                    TransformBlock::Custom(block) => ExpectedBlock::Custom {
+                        id: block.id,
+                        disposition: block.disposition,
+                        height: block.height,
+                    },
+                    TransformBlock::ExcerptHeader {
+                        height,
+                        starts_new_buffer,
+                        ..
+                    } => ExpectedBlock::ExcerptHeader {
+                        height,
+                        starts_new_buffer,
+                    },
+                }
+            }
+        }
+    }
+
+    fn init_test(cx: &mut gpui::AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+    }
+
+    impl TransformBlock {
+        fn as_custom(&self) -> Option<&Block> {
+            match self {
+                TransformBlock::Custom(block) => Some(block),
+                TransformBlock::ExcerptHeader { .. } => None,
+            }
+        }
+    }
+
+    impl BlockSnapshot {
+        fn to_point(&self, point: BlockPoint, bias: Bias) -> Point {
+            self.wrap_snapshot.to_point(self.to_wrap_point(point), bias)
+        }
+    }
+}

crates/editor2/src/display_map/fold_map.rs ๐Ÿ”—

@@ -0,0 +1,1706 @@
+use super::{
+    inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
+    Highlights,
+};
+use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
+use gpui::{color::Color, fonts::HighlightStyle};
+use language::{Chunk, Edit, Point, TextSummary};
+use std::{
+    any::TypeId,
+    cmp::{self, Ordering},
+    iter,
+    ops::{Add, AddAssign, Range, Sub},
+};
+use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct FoldPoint(pub Point);
+
+impl FoldPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(Point::new(row, column))
+    }
+
+    pub fn row(self) -> u32 {
+        self.0.row
+    }
+
+    pub fn column(self) -> u32 {
+        self.0.column
+    }
+
+    pub fn row_mut(&mut self) -> &mut u32 {
+        &mut self.0.row
+    }
+
+    #[cfg(test)]
+    pub fn column_mut(&mut self) -> &mut u32 {
+        &mut self.0.column
+    }
+
+    pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint {
+        let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>();
+        cursor.seek(&self, Bias::Right, &());
+        let overshoot = self.0 - cursor.start().0 .0;
+        InlayPoint(cursor.start().1 .0 + overshoot)
+    }
+
+    pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset {
+        let mut cursor = snapshot
+            .transforms
+            .cursor::<(FoldPoint, TransformSummary)>();
+        cursor.seek(&self, Bias::Right, &());
+        let overshoot = self.0 - cursor.start().1.output.lines;
+        let mut offset = cursor.start().1.output.len;
+        if !overshoot.is_zero() {
+            let transform = cursor.item().expect("display point out of range");
+            assert!(transform.output_text.is_none());
+            let end_inlay_offset = snapshot
+                .inlay_snapshot
+                .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot));
+            offset += end_inlay_offset.0 - cursor.start().1.input.len;
+        }
+        FoldOffset(offset)
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.output.lines;
+    }
+}
+
+pub struct FoldMapWriter<'a>(&'a mut FoldMap);
+
+impl<'a> FoldMapWriter<'a> {
+    pub fn fold<T: ToOffset>(
+        &mut self,
+        ranges: impl IntoIterator<Item = Range<T>>,
+    ) -> (FoldSnapshot, Vec<FoldEdit>) {
+        let mut edits = Vec::new();
+        let mut folds = Vec::new();
+        let snapshot = self.0.snapshot.inlay_snapshot.clone();
+        for range in ranges.into_iter() {
+            let buffer = &snapshot.buffer;
+            let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
+
+            // Ignore any empty ranges.
+            if range.start == range.end {
+                continue;
+            }
+
+            // For now, ignore any ranges that span an excerpt boundary.
+            let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
+            if fold.0.start.excerpt_id != fold.0.end.excerpt_id {
+                continue;
+            }
+
+            folds.push(fold);
+
+            let inlay_range =
+                snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
+            edits.push(InlayEdit {
+                old: inlay_range.clone(),
+                new: inlay_range,
+            });
+        }
+
+        let buffer = &snapshot.buffer;
+        folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer));
+
+        self.0.snapshot.folds = {
+            let mut new_tree = SumTree::new();
+            let mut cursor = self.0.snapshot.folds.cursor::<Fold>();
+            for fold in folds {
+                new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer);
+                new_tree.push(fold, buffer);
+            }
+            new_tree.append(cursor.suffix(buffer), buffer);
+            new_tree
+        };
+
+        consolidate_inlay_edits(&mut edits);
+        let edits = self.0.sync(snapshot.clone(), edits);
+        (self.0.snapshot.clone(), edits)
+    }
+
+    pub fn unfold<T: ToOffset>(
+        &mut self,
+        ranges: impl IntoIterator<Item = Range<T>>,
+        inclusive: bool,
+    ) -> (FoldSnapshot, Vec<FoldEdit>) {
+        let mut edits = Vec::new();
+        let mut fold_ixs_to_delete = Vec::new();
+        let snapshot = self.0.snapshot.inlay_snapshot.clone();
+        let buffer = &snapshot.buffer;
+        for range in ranges.into_iter() {
+            // Remove intersecting folds and add their ranges to edits that are passed to sync.
+            let mut folds_cursor =
+                intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
+            while let Some(fold) = folds_cursor.item() {
+                let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer);
+                if offset_range.end > offset_range.start {
+                    let inlay_range = snapshot.to_inlay_offset(offset_range.start)
+                        ..snapshot.to_inlay_offset(offset_range.end);
+                    edits.push(InlayEdit {
+                        old: inlay_range.clone(),
+                        new: inlay_range,
+                    });
+                }
+                fold_ixs_to_delete.push(*folds_cursor.start());
+                folds_cursor.next(buffer);
+            }
+        }
+
+        fold_ixs_to_delete.sort_unstable();
+        fold_ixs_to_delete.dedup();
+
+        self.0.snapshot.folds = {
+            let mut cursor = self.0.snapshot.folds.cursor::<usize>();
+            let mut folds = SumTree::new();
+            for fold_ix in fold_ixs_to_delete {
+                folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer);
+                cursor.next(buffer);
+            }
+            folds.append(cursor.suffix(buffer), buffer);
+            folds
+        };
+
+        consolidate_inlay_edits(&mut edits);
+        let edits = self.0.sync(snapshot.clone(), edits);
+        (self.0.snapshot.clone(), edits)
+    }
+}
+
+pub struct FoldMap {
+    snapshot: FoldSnapshot,
+    ellipses_color: Option<Color>,
+}
+
+impl FoldMap {
+    pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
+        let this = Self {
+            snapshot: FoldSnapshot {
+                folds: Default::default(),
+                transforms: SumTree::from_item(
+                    Transform {
+                        summary: TransformSummary {
+                            input: inlay_snapshot.text_summary(),
+                            output: inlay_snapshot.text_summary(),
+                        },
+                        output_text: None,
+                    },
+                    &(),
+                ),
+                inlay_snapshot: inlay_snapshot.clone(),
+                version: 0,
+                ellipses_color: None,
+            },
+            ellipses_color: None,
+        };
+        let snapshot = this.snapshot.clone();
+        (this, snapshot)
+    }
+
+    pub fn read(
+        &mut self,
+        inlay_snapshot: InlaySnapshot,
+        edits: Vec<InlayEdit>,
+    ) -> (FoldSnapshot, Vec<FoldEdit>) {
+        let edits = self.sync(inlay_snapshot, edits);
+        self.check_invariants();
+        (self.snapshot.clone(), edits)
+    }
+
+    pub fn write(
+        &mut self,
+        inlay_snapshot: InlaySnapshot,
+        edits: Vec<InlayEdit>,
+    ) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
+        let (snapshot, edits) = self.read(inlay_snapshot, edits);
+        (FoldMapWriter(self), snapshot, edits)
+    }
+
+    pub fn set_ellipses_color(&mut self, color: Color) -> bool {
+        if self.ellipses_color != Some(color) {
+            self.ellipses_color = Some(color);
+            true
+        } else {
+            false
+        }
+    }
+
+    fn check_invariants(&self) {
+        if cfg!(test) {
+            assert_eq!(
+                self.snapshot.transforms.summary().input.len,
+                self.snapshot.inlay_snapshot.len().0,
+                "transform tree does not match inlay snapshot's length"
+            );
+
+            let mut folds = self.snapshot.folds.iter().peekable();
+            while let Some(fold) = folds.next() {
+                if let Some(next_fold) = folds.peek() {
+                    let comparison = fold
+                        .0
+                        .cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
+                    assert!(comparison.is_le());
+                }
+            }
+        }
+    }
+
+    fn sync(
+        &mut self,
+        inlay_snapshot: InlaySnapshot,
+        inlay_edits: Vec<InlayEdit>,
+    ) -> Vec<FoldEdit> {
+        if inlay_edits.is_empty() {
+            if self.snapshot.inlay_snapshot.version != inlay_snapshot.version {
+                self.snapshot.version += 1;
+            }
+            self.snapshot.inlay_snapshot = inlay_snapshot;
+            Vec::new()
+        } else {
+            let mut inlay_edits_iter = inlay_edits.iter().cloned().peekable();
+
+            let mut new_transforms = SumTree::new();
+            let mut cursor = self.snapshot.transforms.cursor::<InlayOffset>();
+            cursor.seek(&InlayOffset(0), Bias::Right, &());
+
+            while let Some(mut edit) = inlay_edits_iter.next() {
+                new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &());
+                edit.new.start -= edit.old.start - *cursor.start();
+                edit.old.start = *cursor.start();
+
+                cursor.seek(&edit.old.end, Bias::Right, &());
+                cursor.next(&());
+
+                let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize;
+                loop {
+                    edit.old.end = *cursor.start();
+
+                    if let Some(next_edit) = inlay_edits_iter.peek() {
+                        if next_edit.old.start > edit.old.end {
+                            break;
+                        }
+
+                        let next_edit = inlay_edits_iter.next().unwrap();
+                        delta += next_edit.new_len().0 as isize - next_edit.old_len().0 as isize;
+
+                        if next_edit.old.end >= edit.old.end {
+                            edit.old.end = next_edit.old.end;
+                            cursor.seek(&edit.old.end, Bias::Right, &());
+                            cursor.next(&());
+                        }
+                    } else {
+                        break;
+                    }
+                }
+
+                edit.new.end =
+                    InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize);
+
+                let anchor = inlay_snapshot
+                    .buffer
+                    .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
+                let mut folds_cursor = self.snapshot.folds.cursor::<Fold>();
+                folds_cursor.seek(
+                    &Fold(anchor..Anchor::max()),
+                    Bias::Left,
+                    &inlay_snapshot.buffer,
+                );
+
+                let mut folds = iter::from_fn({
+                    let inlay_snapshot = &inlay_snapshot;
+                    move || {
+                        let item = folds_cursor.item().map(|f| {
+                            let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
+                            let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer);
+                            inlay_snapshot.to_inlay_offset(buffer_start)
+                                ..inlay_snapshot.to_inlay_offset(buffer_end)
+                        });
+                        folds_cursor.next(&inlay_snapshot.buffer);
+                        item
+                    }
+                })
+                .peekable();
+
+                while folds.peek().map_or(false, |fold| fold.start < edit.new.end) {
+                    let mut fold = folds.next().unwrap();
+                    let sum = new_transforms.summary();
+
+                    assert!(fold.start.0 >= sum.input.len);
+
+                    while folds
+                        .peek()
+                        .map_or(false, |next_fold| next_fold.start <= fold.end)
+                    {
+                        let next_fold = folds.next().unwrap();
+                        if next_fold.end > fold.end {
+                            fold.end = next_fold.end;
+                        }
+                    }
+
+                    if fold.start.0 > sum.input.len {
+                        let text_summary = inlay_snapshot
+                            .text_summary_for_range(InlayOffset(sum.input.len)..fold.start);
+                        new_transforms.push(
+                            Transform {
+                                summary: TransformSummary {
+                                    output: text_summary.clone(),
+                                    input: text_summary,
+                                },
+                                output_text: None,
+                            },
+                            &(),
+                        );
+                    }
+
+                    if fold.end > fold.start {
+                        let output_text = "โ‹ฏ";
+                        new_transforms.push(
+                            Transform {
+                                summary: TransformSummary {
+                                    output: TextSummary::from(output_text),
+                                    input: inlay_snapshot
+                                        .text_summary_for_range(fold.start..fold.end),
+                                },
+                                output_text: Some(output_text),
+                            },
+                            &(),
+                        );
+                    }
+                }
+
+                let sum = new_transforms.summary();
+                if sum.input.len < edit.new.end.0 {
+                    let text_summary = inlay_snapshot
+                        .text_summary_for_range(InlayOffset(sum.input.len)..edit.new.end);
+                    new_transforms.push(
+                        Transform {
+                            summary: TransformSummary {
+                                output: text_summary.clone(),
+                                input: text_summary,
+                            },
+                            output_text: None,
+                        },
+                        &(),
+                    );
+                }
+            }
+
+            new_transforms.append(cursor.suffix(&()), &());
+            if new_transforms.is_empty() {
+                let text_summary = inlay_snapshot.text_summary();
+                new_transforms.push(
+                    Transform {
+                        summary: TransformSummary {
+                            output: text_summary.clone(),
+                            input: text_summary,
+                        },
+                        output_text: None,
+                    },
+                    &(),
+                );
+            }
+
+            drop(cursor);
+
+            let mut fold_edits = Vec::with_capacity(inlay_edits.len());
+            {
+                let mut old_transforms = self
+                    .snapshot
+                    .transforms
+                    .cursor::<(InlayOffset, FoldOffset)>();
+                let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>();
+
+                for mut edit in inlay_edits {
+                    old_transforms.seek(&edit.old.start, Bias::Left, &());
+                    if old_transforms.item().map_or(false, |t| t.is_fold()) {
+                        edit.old.start = old_transforms.start().0;
+                    }
+                    let old_start =
+                        old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0).0;
+
+                    old_transforms.seek_forward(&edit.old.end, Bias::Right, &());
+                    if old_transforms.item().map_or(false, |t| t.is_fold()) {
+                        old_transforms.next(&());
+                        edit.old.end = old_transforms.start().0;
+                    }
+                    let old_end =
+                        old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0).0;
+
+                    new_transforms.seek(&edit.new.start, Bias::Left, &());
+                    if new_transforms.item().map_or(false, |t| t.is_fold()) {
+                        edit.new.start = new_transforms.start().0;
+                    }
+                    let new_start =
+                        new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0).0;
+
+                    new_transforms.seek_forward(&edit.new.end, Bias::Right, &());
+                    if new_transforms.item().map_or(false, |t| t.is_fold()) {
+                        new_transforms.next(&());
+                        edit.new.end = new_transforms.start().0;
+                    }
+                    let new_end =
+                        new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0).0;
+
+                    fold_edits.push(FoldEdit {
+                        old: FoldOffset(old_start)..FoldOffset(old_end),
+                        new: FoldOffset(new_start)..FoldOffset(new_end),
+                    });
+                }
+
+                consolidate_fold_edits(&mut fold_edits);
+            }
+
+            self.snapshot.transforms = new_transforms;
+            self.snapshot.inlay_snapshot = inlay_snapshot;
+            self.snapshot.version += 1;
+            fold_edits
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct FoldSnapshot {
+    transforms: SumTree<Transform>,
+    folds: SumTree<Fold>,
+    pub inlay_snapshot: InlaySnapshot,
+    pub version: usize,
+    pub ellipses_color: Option<Color>,
+}
+
+impl FoldSnapshot {
+    #[cfg(test)]
+    pub fn text(&self) -> String {
+        self.chunks(FoldOffset(0)..self.len(), false, Highlights::default())
+            .map(|c| c.text)
+            .collect()
+    }
+
+    #[cfg(test)]
+    pub fn fold_count(&self) -> usize {
+        self.folds.items(&self.inlay_snapshot.buffer).len()
+    }
+
+    pub fn text_summary_for_range(&self, range: Range<FoldPoint>) -> TextSummary {
+        let mut summary = TextSummary::default();
+
+        let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
+        cursor.seek(&range.start, Bias::Right, &());
+        if let Some(transform) = cursor.item() {
+            let start_in_transform = range.start.0 - cursor.start().0 .0;
+            let end_in_transform = cmp::min(range.end, cursor.end(&()).0).0 - cursor.start().0 .0;
+            if let Some(output_text) = transform.output_text {
+                summary = TextSummary::from(
+                    &output_text
+                        [start_in_transform.column as usize..end_in_transform.column as usize],
+                );
+            } else {
+                let inlay_start = self
+                    .inlay_snapshot
+                    .to_offset(InlayPoint(cursor.start().1 .0 + start_in_transform));
+                let inlay_end = self
+                    .inlay_snapshot
+                    .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
+                summary = self
+                    .inlay_snapshot
+                    .text_summary_for_range(inlay_start..inlay_end);
+            }
+        }
+
+        if range.end > cursor.end(&()).0 {
+            cursor.next(&());
+            summary += &cursor
+                .summary::<_, TransformSummary>(&range.end, Bias::Right, &())
+                .output;
+            if let Some(transform) = cursor.item() {
+                let end_in_transform = range.end.0 - cursor.start().0 .0;
+                if let Some(output_text) = transform.output_text {
+                    summary += TextSummary::from(&output_text[..end_in_transform.column as usize]);
+                } else {
+                    let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1);
+                    let inlay_end = self
+                        .inlay_snapshot
+                        .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
+                    summary += self
+                        .inlay_snapshot
+                        .text_summary_for_range(inlay_start..inlay_end);
+                }
+            }
+        }
+
+        summary
+    }
+
+    pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>();
+        cursor.seek(&point, Bias::Right, &());
+        if cursor.item().map_or(false, |t| t.is_fold()) {
+            if bias == Bias::Left || point == cursor.start().0 {
+                cursor.start().1
+            } else {
+                cursor.end(&()).1
+            }
+        } else {
+            let overshoot = point.0 - cursor.start().0 .0;
+            FoldPoint(cmp::min(
+                cursor.start().1 .0 + overshoot,
+                cursor.end(&()).1 .0,
+            ))
+        }
+    }
+
+    pub fn len(&self) -> FoldOffset {
+        FoldOffset(self.transforms.summary().output.len)
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        let line_start = FoldPoint::new(row, 0).to_offset(self).0;
+        let line_end = if row >= self.max_point().row() {
+            self.len().0
+        } else {
+            FoldPoint::new(row + 1, 0).to_offset(self).0 - 1
+        };
+        (line_end - line_start) as u32
+    }
+
+    pub fn buffer_rows(&self, start_row: u32) -> FoldBufferRows {
+        if start_row > self.transforms.summary().output.lines.row {
+            panic!("invalid display row {}", start_row);
+        }
+
+        let fold_point = FoldPoint::new(start_row, 0);
+        let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
+        cursor.seek(&fold_point, Bias::Left, &());
+
+        let overshoot = fold_point.0 - cursor.start().0 .0;
+        let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot);
+        let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row());
+
+        FoldBufferRows {
+            fold_point,
+            input_buffer_rows,
+            cursor,
+        }
+    }
+
+    pub fn max_point(&self) -> FoldPoint {
+        FoldPoint(self.transforms.summary().output.lines)
+    }
+
+    #[cfg(test)]
+    pub fn longest_row(&self) -> u32 {
+        self.transforms.summary().output.longest_row
+    }
+
+    pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
+    where
+        T: ToOffset,
+    {
+        let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
+        iter::from_fn(move || {
+            let item = folds.item().map(|f| &f.0);
+            folds.next(&self.inlay_snapshot.buffer);
+            item
+        })
+    }
+
+    pub fn intersects_fold<T>(&self, offset: T) -> bool
+    where
+        T: ToOffset,
+    {
+        let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
+        let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
+        let mut cursor = self.transforms.cursor::<InlayOffset>();
+        cursor.seek(&inlay_offset, Bias::Right, &());
+        cursor.item().map_or(false, |t| t.output_text.is_some())
+    }
+
+    pub fn is_line_folded(&self, buffer_row: u32) -> bool {
+        let mut inlay_point = self
+            .inlay_snapshot
+            .to_inlay_point(Point::new(buffer_row, 0));
+        let mut cursor = self.transforms.cursor::<InlayPoint>();
+        cursor.seek(&inlay_point, Bias::Right, &());
+        loop {
+            match cursor.item() {
+                Some(transform) => {
+                    let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point);
+                    if buffer_point.row != buffer_row {
+                        return false;
+                    } else if transform.output_text.is_some() {
+                        return true;
+                    }
+                }
+                None => return false,
+            }
+
+            if cursor.end(&()).row() == inlay_point.row() {
+                cursor.next(&());
+            } else {
+                inlay_point.0 += Point::new(1, 0);
+                cursor.seek(&inlay_point, Bias::Right, &());
+            }
+        }
+    }
+
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<FoldOffset>,
+        language_aware: bool,
+        highlights: Highlights<'a>,
+    ) -> FoldChunks<'a> {
+        let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
+
+        let inlay_end = {
+            transform_cursor.seek(&range.end, Bias::Right, &());
+            let overshoot = range.end.0 - transform_cursor.start().0 .0;
+            transform_cursor.start().1 + InlayOffset(overshoot)
+        };
+
+        let inlay_start = {
+            transform_cursor.seek(&range.start, Bias::Right, &());
+            let overshoot = range.start.0 - transform_cursor.start().0 .0;
+            transform_cursor.start().1 + InlayOffset(overshoot)
+        };
+
+        FoldChunks {
+            transform_cursor,
+            inlay_chunks: self.inlay_snapshot.chunks(
+                inlay_start..inlay_end,
+                language_aware,
+                highlights,
+            ),
+            inlay_chunk: None,
+            inlay_offset: inlay_start,
+            output_offset: range.start.0,
+            max_output_offset: range.end.0,
+            ellipses_color: self.ellipses_color,
+        }
+    }
+
+    pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
+        self.chunks(
+            start.to_offset(self)..self.len(),
+            false,
+            Highlights::default(),
+        )
+        .flat_map(|chunk| chunk.text.chars())
+    }
+
+    #[cfg(test)]
+    pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset {
+        if offset > self.len() {
+            self.len()
+        } else {
+            self.clip_point(offset.to_point(self), bias).to_offset(self)
+        }
+    }
+
+    pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint {
+        let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
+        cursor.seek(&point, Bias::Right, &());
+        if let Some(transform) = cursor.item() {
+            let transform_start = cursor.start().0 .0;
+            if transform.output_text.is_some() {
+                if point.0 == transform_start || matches!(bias, Bias::Left) {
+                    FoldPoint(transform_start)
+                } else {
+                    FoldPoint(cursor.end(&()).0 .0)
+                }
+            } else {
+                let overshoot = InlayPoint(point.0 - transform_start);
+                let inlay_point = cursor.start().1 + overshoot;
+                let clipped_inlay_point = self.inlay_snapshot.clip_point(inlay_point, bias);
+                FoldPoint(cursor.start().0 .0 + (clipped_inlay_point - cursor.start().1).0)
+            }
+        } else {
+            FoldPoint(self.transforms.summary().output.lines)
+        }
+    }
+}
+
+fn intersecting_folds<'a, T>(
+    inlay_snapshot: &'a InlaySnapshot,
+    folds: &'a SumTree<Fold>,
+    range: Range<T>,
+    inclusive: bool,
+) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize>
+where
+    T: ToOffset,
+{
+    let buffer = &inlay_snapshot.buffer;
+    let start = buffer.anchor_before(range.start.to_offset(buffer));
+    let end = buffer.anchor_after(range.end.to_offset(buffer));
+    let mut cursor = folds.filter::<_, usize>(move |summary| {
+        let start_cmp = start.cmp(&summary.max_end, buffer);
+        let end_cmp = end.cmp(&summary.min_start, buffer);
+
+        if inclusive {
+            start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
+        } else {
+            start_cmp == Ordering::Less && end_cmp == Ordering::Greater
+        }
+    });
+    cursor.next(buffer);
+    cursor
+}
+
+fn consolidate_inlay_edits(edits: &mut Vec<InlayEdit>) {
+    edits.sort_unstable_by(|a, b| {
+        a.old
+            .start
+            .cmp(&b.old.start)
+            .then_with(|| b.old.end.cmp(&a.old.end))
+    });
+
+    let mut i = 1;
+    while i < edits.len() {
+        let edit = edits[i].clone();
+        let prev_edit = &mut edits[i - 1];
+        if prev_edit.old.end >= edit.old.start {
+            prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
+            prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
+            prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
+            edits.remove(i);
+            continue;
+        }
+        i += 1;
+    }
+}
+
+fn consolidate_fold_edits(edits: &mut Vec<FoldEdit>) {
+    edits.sort_unstable_by(|a, b| {
+        a.old
+            .start
+            .cmp(&b.old.start)
+            .then_with(|| b.old.end.cmp(&a.old.end))
+    });
+
+    let mut i = 1;
+    while i < edits.len() {
+        let edit = edits[i].clone();
+        let prev_edit = &mut edits[i - 1];
+        if prev_edit.old.end >= edit.old.start {
+            prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
+            prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
+            prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
+            edits.remove(i);
+            continue;
+        }
+        i += 1;
+    }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+struct Transform {
+    summary: TransformSummary,
+    output_text: Option<&'static str>,
+}
+
+impl Transform {
+    fn is_fold(&self) -> bool {
+        self.output_text.is_some()
+    }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+struct TransformSummary {
+    output: TextSummary,
+    input: TextSummary,
+}
+
+impl sum_tree::Item for Transform {
+    type Summary = TransformSummary;
+
+    fn summary(&self) -> Self::Summary {
+        self.summary.clone()
+    }
+}
+
+impl sum_tree::Summary for TransformSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, other: &Self, _: &()) {
+        self.input += &other.input;
+        self.output += &other.output;
+    }
+}
+
+#[derive(Clone, Debug)]
+struct Fold(Range<Anchor>);
+
+impl Default for Fold {
+    fn default() -> Self {
+        Self(Anchor::min()..Anchor::max())
+    }
+}
+
+impl sum_tree::Item for Fold {
+    type Summary = FoldSummary;
+
+    fn summary(&self) -> Self::Summary {
+        FoldSummary {
+            start: self.0.start.clone(),
+            end: self.0.end.clone(),
+            min_start: self.0.start.clone(),
+            max_end: self.0.end.clone(),
+            count: 1,
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+struct FoldSummary {
+    start: Anchor,
+    end: Anchor,
+    min_start: Anchor,
+    max_end: Anchor,
+    count: usize,
+}
+
+impl Default for FoldSummary {
+    fn default() -> Self {
+        Self {
+            start: Anchor::min(),
+            end: Anchor::max(),
+            min_start: Anchor::max(),
+            max_end: Anchor::min(),
+            count: 0,
+        }
+    }
+}
+
+impl sum_tree::Summary for FoldSummary {
+    type Context = MultiBufferSnapshot;
+
+    fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
+        if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less {
+            self.min_start = other.min_start.clone();
+        }
+        if other.max_end.cmp(&self.max_end, buffer) == Ordering::Greater {
+            self.max_end = other.max_end.clone();
+        }
+
+        #[cfg(debug_assertions)]
+        {
+            let start_comparison = self.start.cmp(&other.start, buffer);
+            assert!(start_comparison <= Ordering::Equal);
+            if start_comparison == Ordering::Equal {
+                assert!(self.end.cmp(&other.end, buffer) >= Ordering::Equal);
+            }
+        }
+
+        self.start = other.start.clone();
+        self.end = other.end.clone();
+        self.count += other.count;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold {
+    fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
+        self.0.start = summary.start.clone();
+        self.0.end = summary.end.clone();
+    }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
+    fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
+        self.0.cmp(&other.0, buffer)
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
+    fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
+        *self += summary.count;
+    }
+}
+
+#[derive(Clone)]
+pub struct FoldBufferRows<'a> {
+    cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>,
+    input_buffer_rows: InlayBufferRows<'a>,
+    fold_point: FoldPoint,
+}
+
+impl<'a> Iterator for FoldBufferRows<'a> {
+    type Item = Option<u32>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let mut traversed_fold = false;
+        while self.fold_point > self.cursor.end(&()).0 {
+            self.cursor.next(&());
+            traversed_fold = true;
+            if self.cursor.item().is_none() {
+                break;
+            }
+        }
+
+        if self.cursor.item().is_some() {
+            if traversed_fold {
+                self.input_buffer_rows.seek(self.cursor.start().1.row());
+                self.input_buffer_rows.next();
+            }
+            *self.fold_point.row_mut() += 1;
+            self.input_buffer_rows.next()
+        } else {
+            None
+        }
+    }
+}
+
+pub struct FoldChunks<'a> {
+    transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
+    inlay_chunks: InlayChunks<'a>,
+    inlay_chunk: Option<(InlayOffset, Chunk<'a>)>,
+    inlay_offset: InlayOffset,
+    output_offset: usize,
+    max_output_offset: usize,
+    ellipses_color: Option<Color>,
+}
+
+impl<'a> Iterator for FoldChunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.output_offset >= self.max_output_offset {
+            return None;
+        }
+
+        let transform = self.transform_cursor.item()?;
+
+        // If we're in a fold, then return the fold's display text and
+        // advance the transform and buffer cursors to the end of the fold.
+        if let Some(output_text) = transform.output_text {
+            self.inlay_chunk.take();
+            self.inlay_offset += InlayOffset(transform.summary.input.len);
+            self.inlay_chunks.seek(self.inlay_offset);
+
+            while self.inlay_offset >= self.transform_cursor.end(&()).1
+                && self.transform_cursor.item().is_some()
+            {
+                self.transform_cursor.next(&());
+            }
+
+            self.output_offset += output_text.len();
+            return Some(Chunk {
+                text: output_text,
+                highlight_style: self.ellipses_color.map(|color| HighlightStyle {
+                    color: Some(color),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            });
+        }
+
+        // Retrieve a chunk from the current location in the buffer.
+        if self.inlay_chunk.is_none() {
+            let chunk_offset = self.inlay_chunks.offset();
+            self.inlay_chunk = self.inlay_chunks.next().map(|chunk| (chunk_offset, chunk));
+        }
+
+        // Otherwise, take a chunk from the buffer's text.
+        if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk {
+            let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
+            let transform_end = self.transform_cursor.end(&()).1;
+            let chunk_end = buffer_chunk_end.min(transform_end);
+
+            chunk.text = &chunk.text
+                [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0];
+
+            if chunk_end == transform_end {
+                self.transform_cursor.next(&());
+            } else if chunk_end == buffer_chunk_end {
+                self.inlay_chunk.take();
+            }
+
+            self.inlay_offset = chunk_end;
+            self.output_offset += chunk.text.len();
+            return Some(chunk);
+        }
+
+        None
+    }
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+struct HighlightEndpoint {
+    offset: InlayOffset,
+    is_start: bool,
+    tag: Option<TypeId>,
+    style: HighlightStyle,
+}
+
+impl PartialOrd for HighlightEndpoint {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for HighlightEndpoint {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.offset
+            .cmp(&other.offset)
+            .then_with(|| other.is_start.cmp(&self.is_start))
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct FoldOffset(pub usize);
+
+impl FoldOffset {
+    pub fn to_point(self, snapshot: &FoldSnapshot) -> FoldPoint {
+        let mut cursor = snapshot
+            .transforms
+            .cursor::<(FoldOffset, TransformSummary)>();
+        cursor.seek(&self, Bias::Right, &());
+        let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) {
+            Point::new(0, (self.0 - cursor.start().0 .0) as u32)
+        } else {
+            let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0 .0;
+            let inlay_point = snapshot.inlay_snapshot.to_point(InlayOffset(inlay_offset));
+            inlay_point.0 - cursor.start().1.input.lines
+        };
+        FoldPoint(cursor.start().1.output.lines + overshoot)
+    }
+
+    #[cfg(test)]
+    pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset {
+        let mut cursor = snapshot.transforms.cursor::<(FoldOffset, InlayOffset)>();
+        cursor.seek(&self, Bias::Right, &());
+        let overshoot = self.0 - cursor.start().0 .0;
+        InlayOffset(cursor.start().1 .0 + overshoot)
+    }
+}
+
+impl Add for FoldOffset {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self(self.0 + rhs.0)
+    }
+}
+
+impl AddAssign for FoldOffset {
+    fn add_assign(&mut self, rhs: Self) {
+        self.0 += rhs.0;
+    }
+}
+
+impl Sub for FoldOffset {
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Self(self.0 - rhs.0)
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldOffset {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.output.len;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.input.lines;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.input.len;
+    }
+}
+
+pub type FoldEdit = Edit<FoldOffset>;
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{display_map::inlay_map::InlayMap, MultiBuffer, ToPoint};
+    use collections::HashSet;
+    use rand::prelude::*;
+    use settings::SettingsStore;
+    use std::{env, mem};
+    use text::Patch;
+    use util::test::sample_text;
+    use util::RandomCharIter;
+    use Bias::{Left, Right};
+
+    #[gpui::test]
+    fn test_basic_folds(cx: &mut gpui::AppContext) {
+        init_test(cx);
+        let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
+        let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+        let (mut writer, _, _) = map.write(inlay_snapshot, vec![]);
+        let (snapshot2, edits) = writer.fold(vec![
+            Point::new(0, 2)..Point::new(2, 2),
+            Point::new(2, 4)..Point::new(4, 1),
+        ]);
+        assert_eq!(snapshot2.text(), "aaโ‹ฏccโ‹ฏeeeee");
+        assert_eq!(
+            edits,
+            &[
+                FoldEdit {
+                    old: FoldOffset(2)..FoldOffset(16),
+                    new: FoldOffset(2)..FoldOffset(5),
+                },
+                FoldEdit {
+                    old: FoldOffset(18)..FoldOffset(29),
+                    new: FoldOffset(7)..FoldOffset(10)
+                },
+            ]
+        );
+
+        let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+            buffer.edit(
+                vec![
+                    (Point::new(0, 0)..Point::new(0, 1), "123"),
+                    (Point::new(2, 3)..Point::new(2, 3), "123"),
+                ],
+                None,
+                cx,
+            );
+            buffer.snapshot(cx)
+        });
+
+        let (inlay_snapshot, inlay_edits) =
+            inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+        let (snapshot3, edits) = map.read(inlay_snapshot, inlay_edits);
+        assert_eq!(snapshot3.text(), "123aโ‹ฏc123cโ‹ฏeeeee");
+        assert_eq!(
+            edits,
+            &[
+                FoldEdit {
+                    old: FoldOffset(0)..FoldOffset(1),
+                    new: FoldOffset(0)..FoldOffset(3),
+                },
+                FoldEdit {
+                    old: FoldOffset(6)..FoldOffset(6),
+                    new: FoldOffset(8)..FoldOffset(11),
+                },
+            ]
+        );
+
+        let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+            buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx);
+            buffer.snapshot(cx)
+        });
+        let (inlay_snapshot, inlay_edits) =
+            inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+        let (snapshot4, _) = map.read(inlay_snapshot.clone(), inlay_edits);
+        assert_eq!(snapshot4.text(), "123aโ‹ฏc123456eee");
+
+        let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+        writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
+        let (snapshot5, _) = map.read(inlay_snapshot.clone(), vec![]);
+        assert_eq!(snapshot5.text(), "123aโ‹ฏc123456eee");
+
+        let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+        writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
+        let (snapshot6, _) = map.read(inlay_snapshot, vec![]);
+        assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee");
+    }
+
+    #[gpui::test]
+    fn test_adjacent_folds(cx: &mut gpui::AppContext) {
+        init_test(cx);
+        let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
+        let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+
+        {
+            let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+            let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+            writer.fold(vec![5..8]);
+            let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+            assert_eq!(snapshot.text(), "abcdeโ‹ฏijkl");
+
+            // Create an fold adjacent to the start of the first fold.
+            let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+            writer.fold(vec![0..1, 2..5]);
+            let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+            assert_eq!(snapshot.text(), "โ‹ฏbโ‹ฏijkl");
+
+            // Create an fold adjacent to the end of the first fold.
+            let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+            writer.fold(vec![11..11, 8..10]);
+            let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+            assert_eq!(snapshot.text(), "โ‹ฏbโ‹ฏkl");
+        }
+
+        {
+            let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+            // Create two adjacent folds.
+            let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+            writer.fold(vec![0..2, 2..5]);
+            let (snapshot, _) = map.read(inlay_snapshot, vec![]);
+            assert_eq!(snapshot.text(), "โ‹ฏfghijkl");
+
+            // Edit within one of the folds.
+            let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+                buffer.edit([(0..1, "12345")], None, cx);
+                buffer.snapshot(cx)
+            });
+            let (inlay_snapshot, inlay_edits) =
+                inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+            let (snapshot, _) = map.read(inlay_snapshot, inlay_edits);
+            assert_eq!(snapshot.text(), "12345โ‹ฏfghijkl");
+        }
+    }
+
+    #[gpui::test]
+    fn test_overlapping_folds(cx: &mut gpui::AppContext) {
+        let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+        let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+        let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+        writer.fold(vec![
+            Point::new(0, 2)..Point::new(2, 2),
+            Point::new(0, 4)..Point::new(1, 0),
+            Point::new(1, 2)..Point::new(3, 2),
+            Point::new(3, 1)..Point::new(4, 1),
+        ]);
+        let (snapshot, _) = map.read(inlay_snapshot, vec![]);
+        assert_eq!(snapshot.text(), "aaโ‹ฏeeeee");
+    }
+
+    #[gpui::test]
+    fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) {
+        init_test(cx);
+        let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
+        let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+        let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+        writer.fold(vec![
+            Point::new(0, 2)..Point::new(2, 2),
+            Point::new(3, 1)..Point::new(4, 1),
+        ]);
+        let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+        assert_eq!(snapshot.text(), "aaโ‹ฏcccc\ndโ‹ฏeeeee");
+
+        let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+            buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx);
+            buffer.snapshot(cx)
+        });
+        let (inlay_snapshot, inlay_edits) =
+            inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+        let (snapshot, _) = map.read(inlay_snapshot, inlay_edits);
+        assert_eq!(snapshot.text(), "aaโ‹ฏeeeee");
+    }
+
+    #[gpui::test]
+    fn test_folds_in_range(cx: &mut gpui::AppContext) {
+        let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+        let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+        writer.fold(vec![
+            Point::new(0, 2)..Point::new(2, 2),
+            Point::new(0, 4)..Point::new(1, 0),
+            Point::new(1, 2)..Point::new(3, 2),
+            Point::new(3, 1)..Point::new(4, 1),
+        ]);
+        let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+        let fold_ranges = snapshot
+            .folds_in_range(Point::new(1, 0)..Point::new(1, 3))
+            .map(|fold| fold.start.to_point(&buffer_snapshot)..fold.end.to_point(&buffer_snapshot))
+            .collect::<Vec<_>>();
+        assert_eq!(
+            fold_ranges,
+            vec![
+                Point::new(0, 2)..Point::new(2, 2),
+                Point::new(1, 2)..Point::new(3, 2)
+            ]
+        );
+    }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_folds(cx: &mut gpui::AppContext, mut rng: StdRng) {
+        init_test(cx);
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let len = rng.gen_range(0..10);
+        let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+        let buffer = if rng.gen() {
+            MultiBuffer::build_simple(&text, cx)
+        } else {
+            MultiBuffer::build_random(&mut rng, cx)
+        };
+        let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+        let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+        let mut snapshot_edits = Vec::new();
+
+        let mut next_inlay_id = 0;
+        for _ in 0..operations {
+            log::info!("text: {:?}", buffer_snapshot.text());
+            let mut buffer_edits = Vec::new();
+            let mut inlay_edits = Vec::new();
+            match rng.gen_range(0..=100) {
+                0..=39 => {
+                    snapshot_edits.extend(map.randomly_mutate(&mut rng));
+                }
+                40..=59 => {
+                    let (_, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+                    inlay_edits = edits;
+                }
+                _ => buffer.update(cx, |buffer, cx| {
+                    let subscription = buffer.subscribe();
+                    let edit_count = rng.gen_range(1..=5);
+                    buffer.randomly_mutate(&mut rng, edit_count, cx);
+                    buffer_snapshot = buffer.snapshot(cx);
+                    let edits = subscription.consume().into_inner();
+                    log::info!("editing {:?}", edits);
+                    buffer_edits.extend(edits);
+                }),
+            };
+
+            let (inlay_snapshot, new_inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            log::info!("inlay text {:?}", inlay_snapshot.text());
+
+            let inlay_edits = Patch::new(inlay_edits)
+                .compose(new_inlay_edits)
+                .into_inner();
+            let (snapshot, edits) = map.read(inlay_snapshot.clone(), inlay_edits);
+            snapshot_edits.push((snapshot.clone(), edits));
+
+            let mut expected_text: String = inlay_snapshot.text().to_string();
+            for fold_range in map.merged_fold_ranges().into_iter().rev() {
+                let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start);
+                let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end);
+                expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "โ‹ฏ");
+            }
+
+            assert_eq!(snapshot.text(), expected_text);
+            log::info!(
+                "fold text {:?} ({} lines)",
+                expected_text,
+                expected_text.matches('\n').count() + 1
+            );
+
+            let mut prev_row = 0;
+            let mut expected_buffer_rows = Vec::new();
+            for fold_range in map.merged_fold_ranges().into_iter() {
+                let fold_start = inlay_snapshot
+                    .to_point(inlay_snapshot.to_inlay_offset(fold_range.start))
+                    .row();
+                let fold_end = inlay_snapshot
+                    .to_point(inlay_snapshot.to_inlay_offset(fold_range.end))
+                    .row();
+                expected_buffer_rows.extend(
+                    inlay_snapshot
+                        .buffer_rows(prev_row)
+                        .take((1 + fold_start - prev_row) as usize),
+                );
+                prev_row = 1 + fold_end;
+            }
+            expected_buffer_rows.extend(inlay_snapshot.buffer_rows(prev_row));
+
+            assert_eq!(
+                expected_buffer_rows.len(),
+                expected_text.matches('\n').count() + 1,
+                "wrong expected buffer rows {:?}. text: {:?}",
+                expected_buffer_rows,
+                expected_text
+            );
+
+            for (output_row, line) in expected_text.lines().enumerate() {
+                let line_len = snapshot.line_len(output_row as u32);
+                assert_eq!(line_len, line.len() as u32);
+            }
+
+            let longest_row = snapshot.longest_row();
+            let longest_char_column = expected_text
+                .split('\n')
+                .nth(longest_row as usize)
+                .unwrap()
+                .chars()
+                .count();
+            let mut fold_point = FoldPoint::new(0, 0);
+            let mut fold_offset = FoldOffset(0);
+            let mut char_column = 0;
+            for c in expected_text.chars() {
+                let inlay_point = fold_point.to_inlay_point(&snapshot);
+                let inlay_offset = fold_offset.to_inlay_offset(&snapshot);
+                assert_eq!(
+                    snapshot.to_fold_point(inlay_point, Right),
+                    fold_point,
+                    "{:?} -> fold point",
+                    inlay_point,
+                );
+                assert_eq!(
+                    inlay_snapshot.to_offset(inlay_point),
+                    inlay_offset,
+                    "inlay_snapshot.to_offset({:?})",
+                    inlay_point,
+                );
+                assert_eq!(
+                    fold_point.to_offset(&snapshot),
+                    fold_offset,
+                    "fold_point.to_offset({:?})",
+                    fold_point,
+                );
+
+                if c == '\n' {
+                    *fold_point.row_mut() += 1;
+                    *fold_point.column_mut() = 0;
+                    char_column = 0;
+                } else {
+                    *fold_point.column_mut() += c.len_utf8() as u32;
+                    char_column += 1;
+                }
+                fold_offset.0 += c.len_utf8();
+                if char_column > longest_char_column {
+                    panic!(
+                        "invalid longest row {:?} (chars {}), found row {:?} (chars: {})",
+                        longest_row,
+                        longest_char_column,
+                        fold_point.row(),
+                        char_column
+                    );
+                }
+            }
+
+            for _ in 0..5 {
+                let mut start = snapshot
+                    .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Left);
+                let mut end = snapshot
+                    .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Right);
+                if start > end {
+                    mem::swap(&mut start, &mut end);
+                }
+
+                let text = &expected_text[start.0..end.0];
+                assert_eq!(
+                    snapshot
+                        .chunks(start..end, false, Highlights::default())
+                        .map(|c| c.text)
+                        .collect::<String>(),
+                    text,
+                );
+            }
+
+            let mut fold_row = 0;
+            while fold_row < expected_buffer_rows.len() as u32 {
+                assert_eq!(
+                    snapshot.buffer_rows(fold_row).collect::<Vec<_>>(),
+                    expected_buffer_rows[(fold_row as usize)..],
+                    "wrong buffer rows starting at fold row {}",
+                    fold_row,
+                );
+                fold_row += 1;
+            }
+
+            let folded_buffer_rows = map
+                .merged_fold_ranges()
+                .iter()
+                .flat_map(|range| {
+                    let start_row = range.start.to_point(&buffer_snapshot).row;
+                    let end = range.end.to_point(&buffer_snapshot);
+                    if end.column == 0 {
+                        start_row..end.row
+                    } else {
+                        start_row..end.row + 1
+                    }
+                })
+                .collect::<HashSet<_>>();
+            for row in 0..=buffer_snapshot.max_point().row {
+                assert_eq!(
+                    snapshot.is_line_folded(row),
+                    folded_buffer_rows.contains(&row),
+                    "expected buffer row {}{} to be folded",
+                    row,
+                    if folded_buffer_rows.contains(&row) {
+                        ""
+                    } else {
+                        " not"
+                    }
+                );
+            }
+
+            for _ in 0..5 {
+                let end =
+                    buffer_snapshot.clip_offset(rng.gen_range(0..=buffer_snapshot.len()), Right);
+                let start = buffer_snapshot.clip_offset(rng.gen_range(0..=end), Left);
+                let expected_folds = map
+                    .snapshot
+                    .folds
+                    .items(&buffer_snapshot)
+                    .into_iter()
+                    .filter(|fold| {
+                        let start = buffer_snapshot.anchor_before(start);
+                        let end = buffer_snapshot.anchor_after(end);
+                        start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
+                            && end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
+                    })
+                    .map(|fold| fold.0)
+                    .collect::<Vec<_>>();
+
+                assert_eq!(
+                    snapshot
+                        .folds_in_range(start..end)
+                        .cloned()
+                        .collect::<Vec<_>>(),
+                    expected_folds
+                );
+            }
+
+            let text = snapshot.text();
+            for _ in 0..5 {
+                let start_row = rng.gen_range(0..=snapshot.max_point().row());
+                let start_column = rng.gen_range(0..=snapshot.line_len(start_row));
+                let end_row = rng.gen_range(0..=snapshot.max_point().row());
+                let end_column = rng.gen_range(0..=snapshot.line_len(end_row));
+                let mut start =
+                    snapshot.clip_point(FoldPoint::new(start_row, start_column), Bias::Left);
+                let mut end = snapshot.clip_point(FoldPoint::new(end_row, end_column), Bias::Right);
+                if start > end {
+                    mem::swap(&mut start, &mut end);
+                }
+
+                let lines = start..end;
+                let bytes = start.to_offset(&snapshot)..end.to_offset(&snapshot);
+                assert_eq!(
+                    snapshot.text_summary_for_range(lines),
+                    TextSummary::from(&text[bytes.start.0..bytes.end.0])
+                )
+            }
+
+            let mut text = initial_snapshot.text();
+            for (snapshot, edits) in snapshot_edits.drain(..) {
+                let new_text = snapshot.text();
+                for edit in edits {
+                    let old_bytes = edit.new.start.0..edit.new.start.0 + edit.old_len().0;
+                    let new_bytes = edit.new.start.0..edit.new.end.0;
+                    text.replace_range(old_bytes, &new_text[new_bytes]);
+                }
+
+                assert_eq!(text, new_text);
+                initial_snapshot = snapshot;
+            }
+        }
+    }
+
+    #[gpui::test]
+    fn test_buffer_rows(cx: &mut gpui::AppContext) {
+        let text = sample_text(6, 6, 'a') + "\n";
+        let buffer = MultiBuffer::build_simple(&text, cx);
+
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+        let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+        let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+        writer.fold(vec![
+            Point::new(0, 2)..Point::new(2, 2),
+            Point::new(3, 1)..Point::new(4, 1),
+        ]);
+
+        let (snapshot, _) = map.read(inlay_snapshot, vec![]);
+        assert_eq!(snapshot.text(), "aaโ‹ฏcccc\ndโ‹ฏeeeee\nffffff\n");
+        assert_eq!(
+            snapshot.buffer_rows(0).collect::<Vec<_>>(),
+            [Some(0), Some(3), Some(5), Some(6)]
+        );
+        assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]);
+    }
+
+    fn init_test(cx: &mut gpui::AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+    }
+
+    impl FoldMap {
+        fn merged_fold_ranges(&self) -> Vec<Range<usize>> {
+            let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
+            let buffer = &inlay_snapshot.buffer;
+            let mut folds = self.snapshot.folds.items(buffer);
+            // Ensure sorting doesn't change how folds get merged and displayed.
+            folds.sort_by(|a, b| a.0.cmp(&b.0, buffer));
+            let mut fold_ranges = folds
+                .iter()
+                .map(|fold| fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer))
+                .peekable();
+
+            let mut merged_ranges = Vec::new();
+            while let Some(mut fold_range) = fold_ranges.next() {
+                while let Some(next_range) = fold_ranges.peek() {
+                    if fold_range.end >= next_range.start {
+                        if next_range.end > fold_range.end {
+                            fold_range.end = next_range.end;
+                        }
+                        fold_ranges.next();
+                    } else {
+                        break;
+                    }
+                }
+                if fold_range.end > fold_range.start {
+                    merged_ranges.push(fold_range);
+                }
+            }
+            merged_ranges
+        }
+
+        pub fn randomly_mutate(
+            &mut self,
+            rng: &mut impl Rng,
+        ) -> Vec<(FoldSnapshot, Vec<FoldEdit>)> {
+            let mut snapshot_edits = Vec::new();
+            match rng.gen_range(0..=100) {
+                0..=39 if !self.snapshot.folds.is_empty() => {
+                    let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
+                    let buffer = &inlay_snapshot.buffer;
+                    let mut to_unfold = Vec::new();
+                    for _ in 0..rng.gen_range(1..=3) {
+                        let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
+                        let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+                        to_unfold.push(start..end);
+                    }
+                    log::info!("unfolding {:?}", to_unfold);
+                    let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
+                    snapshot_edits.push((snapshot, edits));
+                    let (snapshot, edits) = writer.fold(to_unfold);
+                    snapshot_edits.push((snapshot, edits));
+                }
+                _ => {
+                    let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
+                    let buffer = &inlay_snapshot.buffer;
+                    let mut to_fold = Vec::new();
+                    for _ in 0..rng.gen_range(1..=2) {
+                        let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
+                        let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+                        to_fold.push(start..end);
+                    }
+                    log::info!("folding {:?}", to_fold);
+                    let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
+                    snapshot_edits.push((snapshot, edits));
+                    let (snapshot, edits) = writer.fold(to_fold);
+                    snapshot_edits.push((snapshot, edits));
+                }
+            }
+            snapshot_edits
+        }
+    }
+}

crates/editor2/src/display_map/inlay_map.rs ๐Ÿ”—

@@ -0,0 +1,1895 @@
+use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset};
+use collections::{BTreeMap, BTreeSet};
+use gpui::fonts::HighlightStyle;
+use language::{Chunk, Edit, Point, TextSummary};
+use multi_buffer::{MultiBufferChunks, MultiBufferRows};
+use std::{
+    any::TypeId,
+    cmp,
+    iter::Peekable,
+    ops::{Add, AddAssign, Range, Sub, SubAssign},
+    sync::Arc,
+    vec,
+};
+use sum_tree::{Bias, Cursor, SumTree, TreeMap};
+use text::{Patch, Rope};
+
+use super::Highlights;
+
+pub struct InlayMap {
+    snapshot: InlaySnapshot,
+    inlays: Vec<Inlay>,
+}
+
+#[derive(Clone)]
+pub struct InlaySnapshot {
+    pub buffer: MultiBufferSnapshot,
+    transforms: SumTree<Transform>,
+    pub version: usize,
+}
+
+#[derive(Clone, Debug)]
+enum Transform {
+    Isomorphic(TextSummary),
+    Inlay(Inlay),
+}
+
+#[derive(Debug, Clone)]
+pub struct Inlay {
+    pub id: InlayId,
+    pub position: Anchor,
+    pub text: text::Rope,
+}
+
+impl Inlay {
+    pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self {
+        let mut text = hint.text();
+        if hint.padding_right && !text.ends_with(' ') {
+            text.push(' ');
+        }
+        if hint.padding_left && !text.starts_with(' ') {
+            text.insert(0, ' ');
+        }
+        Self {
+            id: InlayId::Hint(id),
+            position,
+            text: text.into(),
+        }
+    }
+
+    pub fn suggestion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+        Self {
+            id: InlayId::Suggestion(id),
+            position,
+            text: text.into(),
+        }
+    }
+}
+
+impl sum_tree::Item for Transform {
+    type Summary = TransformSummary;
+
+    fn summary(&self) -> Self::Summary {
+        match self {
+            Transform::Isomorphic(summary) => TransformSummary {
+                input: summary.clone(),
+                output: summary.clone(),
+            },
+            Transform::Inlay(inlay) => TransformSummary {
+                input: TextSummary::default(),
+                output: inlay.text.summary(),
+            },
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+struct TransformSummary {
+    input: TextSummary,
+    output: TextSummary,
+}
+
+impl sum_tree::Summary for TransformSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, other: &Self, _: &()) {
+        self.input += &other.input;
+        self.output += &other.output;
+    }
+}
+
+pub type InlayEdit = Edit<InlayOffset>;
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayOffset(pub usize);
+
+impl Add for InlayOffset {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self(self.0 + rhs.0)
+    }
+}
+
+impl Sub for InlayOffset {
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Self(self.0 - rhs.0)
+    }
+}
+
+impl AddAssign for InlayOffset {
+    fn add_assign(&mut self, rhs: Self) {
+        self.0 += rhs.0;
+    }
+}
+
+impl SubAssign for InlayOffset {
+    fn sub_assign(&mut self, rhs: Self) {
+        self.0 -= rhs.0;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.output.len;
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayPoint(pub Point);
+
+impl Add for InlayPoint {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self(self.0 + rhs.0)
+    }
+}
+
+impl Sub for InlayPoint {
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Self(self.0 - rhs.0)
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.output.lines;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        *self += &summary.input.len;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        *self += &summary.input.lines;
+    }
+}
+
+#[derive(Clone)]
+pub struct InlayBufferRows<'a> {
+    transforms: Cursor<'a, Transform, (InlayPoint, Point)>,
+    buffer_rows: MultiBufferRows<'a>,
+    inlay_row: u32,
+    max_buffer_row: u32,
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+struct HighlightEndpoint {
+    offset: InlayOffset,
+    is_start: bool,
+    tag: Option<TypeId>,
+    style: HighlightStyle,
+}
+
+impl PartialOrd for HighlightEndpoint {
+    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for HighlightEndpoint {
+    fn cmp(&self, other: &Self) -> cmp::Ordering {
+        self.offset
+            .cmp(&other.offset)
+            .then_with(|| other.is_start.cmp(&self.is_start))
+    }
+}
+
+pub struct InlayChunks<'a> {
+    transforms: Cursor<'a, Transform, (InlayOffset, usize)>,
+    buffer_chunks: MultiBufferChunks<'a>,
+    buffer_chunk: Option<Chunk<'a>>,
+    inlay_chunks: Option<text::Chunks<'a>>,
+    inlay_chunk: Option<&'a str>,
+    output_offset: InlayOffset,
+    max_output_offset: InlayOffset,
+    inlay_highlight_style: Option<HighlightStyle>,
+    suggestion_highlight_style: Option<HighlightStyle>,
+    highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
+    active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
+    highlights: Highlights<'a>,
+    snapshot: &'a InlaySnapshot,
+}
+
+impl<'a> InlayChunks<'a> {
+    pub fn seek(&mut self, offset: InlayOffset) {
+        self.transforms.seek(&offset, Bias::Right, &());
+
+        let buffer_offset = self.snapshot.to_buffer_offset(offset);
+        self.buffer_chunks.seek(buffer_offset);
+        self.inlay_chunks = None;
+        self.buffer_chunk = None;
+        self.output_offset = offset;
+    }
+
+    pub fn offset(&self) -> InlayOffset {
+        self.output_offset
+    }
+}
+
+impl<'a> Iterator for InlayChunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.output_offset == self.max_output_offset {
+            return None;
+        }
+
+        let mut next_highlight_endpoint = InlayOffset(usize::MAX);
+        while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
+            if endpoint.offset <= self.output_offset {
+                if endpoint.is_start {
+                    self.active_highlights.insert(endpoint.tag, endpoint.style);
+                } else {
+                    self.active_highlights.remove(&endpoint.tag);
+                }
+                self.highlight_endpoints.next();
+            } else {
+                next_highlight_endpoint = endpoint.offset;
+                break;
+            }
+        }
+
+        let chunk = match self.transforms.item()? {
+            Transform::Isomorphic(_) => {
+                let chunk = self
+                    .buffer_chunk
+                    .get_or_insert_with(|| self.buffer_chunks.next().unwrap());
+                if chunk.text.is_empty() {
+                    *chunk = self.buffer_chunks.next().unwrap();
+                }
+
+                let (prefix, suffix) = chunk.text.split_at(
+                    chunk
+                        .text
+                        .len()
+                        .min(self.transforms.end(&()).0 .0 - self.output_offset.0)
+                        .min(next_highlight_endpoint.0 - self.output_offset.0),
+                );
+
+                chunk.text = suffix;
+                self.output_offset.0 += prefix.len();
+                let mut prefix = Chunk {
+                    text: prefix,
+                    ..chunk.clone()
+                };
+                if !self.active_highlights.is_empty() {
+                    let mut highlight_style = HighlightStyle::default();
+                    for active_highlight in self.active_highlights.values() {
+                        highlight_style.highlight(*active_highlight);
+                    }
+                    prefix.highlight_style = Some(highlight_style);
+                }
+                prefix
+            }
+            Transform::Inlay(inlay) => {
+                let mut inlay_style_and_highlight = None;
+                if let Some(inlay_highlights) = self.highlights.inlay_highlights {
+                    for (_, inlay_id_to_data) in inlay_highlights.iter() {
+                        let style_and_highlight = inlay_id_to_data.get(&inlay.id);
+                        if style_and_highlight.is_some() {
+                            inlay_style_and_highlight = style_and_highlight;
+                            break;
+                        }
+                    }
+                }
+
+                let mut highlight_style = match inlay.id {
+                    InlayId::Suggestion(_) => self.suggestion_highlight_style,
+                    InlayId::Hint(_) => self.inlay_highlight_style,
+                };
+                let next_inlay_highlight_endpoint;
+                let offset_in_inlay = self.output_offset - self.transforms.start().0;
+                if let Some((style, highlight)) = inlay_style_and_highlight {
+                    let range = &highlight.range;
+                    if offset_in_inlay.0 < range.start {
+                        next_inlay_highlight_endpoint = range.start - offset_in_inlay.0;
+                    } else if offset_in_inlay.0 >= range.end {
+                        next_inlay_highlight_endpoint = usize::MAX;
+                    } else {
+                        next_inlay_highlight_endpoint = range.end - offset_in_inlay.0;
+                        highlight_style
+                            .get_or_insert_with(|| Default::default())
+                            .highlight(style.clone());
+                    }
+                } else {
+                    next_inlay_highlight_endpoint = usize::MAX;
+                }
+
+                let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
+                    let start = offset_in_inlay;
+                    let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0)
+                        - self.transforms.start().0;
+                    inlay.text.chunks_in_range(start.0..end.0)
+                });
+                let inlay_chunk = self
+                    .inlay_chunk
+                    .get_or_insert_with(|| inlay_chunks.next().unwrap());
+                let (chunk, remainder) =
+                    inlay_chunk.split_at(inlay_chunk.len().min(next_inlay_highlight_endpoint));
+                *inlay_chunk = remainder;
+                if inlay_chunk.is_empty() {
+                    self.inlay_chunk = None;
+                }
+
+                self.output_offset.0 += chunk.len();
+
+                if !self.active_highlights.is_empty() {
+                    for active_highlight in self.active_highlights.values() {
+                        highlight_style
+                            .get_or_insert(Default::default())
+                            .highlight(*active_highlight);
+                    }
+                }
+                Chunk {
+                    text: chunk,
+                    highlight_style,
+                    ..Default::default()
+                }
+            }
+        };
+
+        if self.output_offset == self.transforms.end(&()).0 {
+            self.inlay_chunks = None;
+            self.transforms.next(&());
+        }
+
+        Some(chunk)
+    }
+}
+
+impl<'a> InlayBufferRows<'a> {
+    pub fn seek(&mut self, row: u32) {
+        let inlay_point = InlayPoint::new(row, 0);
+        self.transforms.seek(&inlay_point, Bias::Left, &());
+
+        let mut buffer_point = self.transforms.start().1;
+        let buffer_row = if row == 0 {
+            0
+        } else {
+            match self.transforms.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    buffer_point += inlay_point.0 - self.transforms.start().0 .0;
+                    buffer_point.row
+                }
+                _ => cmp::min(buffer_point.row + 1, self.max_buffer_row),
+            }
+        };
+        self.inlay_row = inlay_point.row();
+        self.buffer_rows.seek(buffer_row);
+    }
+}
+
+impl<'a> Iterator for InlayBufferRows<'a> {
+    type Item = Option<u32>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let buffer_row = if self.inlay_row == 0 {
+            self.buffer_rows.next().unwrap()
+        } else {
+            match self.transforms.item()? {
+                Transform::Inlay(_) => None,
+                Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(),
+            }
+        };
+
+        self.inlay_row += 1;
+        self.transforms
+            .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &());
+
+        Some(buffer_row)
+    }
+}
+
+impl InlayPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(Point::new(row, column))
+    }
+
+    pub fn row(self) -> u32 {
+        self.0.row
+    }
+}
+
+impl InlayMap {
+    pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) {
+        let version = 0;
+        let snapshot = InlaySnapshot {
+            buffer: buffer.clone(),
+            transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), &()),
+            version,
+        };
+
+        (
+            Self {
+                snapshot: snapshot.clone(),
+                inlays: Vec::new(),
+            },
+            snapshot,
+        )
+    }
+
+    pub fn sync(
+        &mut self,
+        buffer_snapshot: MultiBufferSnapshot,
+        mut buffer_edits: Vec<text::Edit<usize>>,
+    ) -> (InlaySnapshot, Vec<InlayEdit>) {
+        let snapshot = &mut self.snapshot;
+
+        if buffer_edits.is_empty() {
+            if snapshot.buffer.trailing_excerpt_update_count()
+                != buffer_snapshot.trailing_excerpt_update_count()
+            {
+                buffer_edits.push(Edit {
+                    old: snapshot.buffer.len()..snapshot.buffer.len(),
+                    new: buffer_snapshot.len()..buffer_snapshot.len(),
+                });
+            }
+        }
+
+        if buffer_edits.is_empty() {
+            if snapshot.buffer.edit_count() != buffer_snapshot.edit_count()
+                || snapshot.buffer.parse_count() != buffer_snapshot.parse_count()
+                || snapshot.buffer.diagnostics_update_count()
+                    != buffer_snapshot.diagnostics_update_count()
+                || snapshot.buffer.git_diff_update_count()
+                    != buffer_snapshot.git_diff_update_count()
+                || snapshot.buffer.trailing_excerpt_update_count()
+                    != buffer_snapshot.trailing_excerpt_update_count()
+            {
+                snapshot.version += 1;
+            }
+
+            snapshot.buffer = buffer_snapshot;
+            (snapshot.clone(), Vec::new())
+        } else {
+            let mut inlay_edits = Patch::default();
+            let mut new_transforms = SumTree::new();
+            let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>();
+            let mut buffer_edits_iter = buffer_edits.iter().peekable();
+            while let Some(buffer_edit) = buffer_edits_iter.next() {
+                new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &());
+                if let Some(Transform::Isomorphic(transform)) = cursor.item() {
+                    if cursor.end(&()).0 == buffer_edit.old.start {
+                        push_isomorphic(&mut new_transforms, transform.clone());
+                        cursor.next(&());
+                    }
+                }
+
+                // Remove all the inlays and transforms contained by the edit.
+                let old_start =
+                    cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0);
+                cursor.seek(&buffer_edit.old.end, Bias::Right, &());
+                let old_end =
+                    cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0);
+
+                // Push the unchanged prefix.
+                let prefix_start = new_transforms.summary().input.len;
+                let prefix_end = buffer_edit.new.start;
+                push_isomorphic(
+                    &mut new_transforms,
+                    buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+                );
+                let new_start = InlayOffset(new_transforms.summary().output.len);
+
+                let start_ix = match self.inlays.binary_search_by(|probe| {
+                    probe
+                        .position
+                        .to_offset(&buffer_snapshot)
+                        .cmp(&buffer_edit.new.start)
+                        .then(std::cmp::Ordering::Greater)
+                }) {
+                    Ok(ix) | Err(ix) => ix,
+                };
+
+                for inlay in &self.inlays[start_ix..] {
+                    let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
+                    if buffer_offset > buffer_edit.new.end {
+                        break;
+                    }
+
+                    let prefix_start = new_transforms.summary().input.len;
+                    let prefix_end = buffer_offset;
+                    push_isomorphic(
+                        &mut new_transforms,
+                        buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+                    );
+
+                    if inlay.position.is_valid(&buffer_snapshot) {
+                        new_transforms.push(Transform::Inlay(inlay.clone()), &());
+                    }
+                }
+
+                // Apply the rest of the edit.
+                let transform_start = new_transforms.summary().input.len;
+                push_isomorphic(
+                    &mut new_transforms,
+                    buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end),
+                );
+                let new_end = InlayOffset(new_transforms.summary().output.len);
+                inlay_edits.push(Edit {
+                    old: old_start..old_end,
+                    new: new_start..new_end,
+                });
+
+                // If the next edit doesn't intersect the current isomorphic transform, then
+                // we can push its remainder.
+                if buffer_edits_iter
+                    .peek()
+                    .map_or(true, |edit| edit.old.start >= cursor.end(&()).0)
+                {
+                    let transform_start = new_transforms.summary().input.len;
+                    let transform_end =
+                        buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end);
+                    push_isomorphic(
+                        &mut new_transforms,
+                        buffer_snapshot.text_summary_for_range(transform_start..transform_end),
+                    );
+                    cursor.next(&());
+                }
+            }
+
+            new_transforms.append(cursor.suffix(&()), &());
+            if new_transforms.is_empty() {
+                new_transforms.push(Transform::Isomorphic(Default::default()), &());
+            }
+
+            drop(cursor);
+            snapshot.transforms = new_transforms;
+            snapshot.version += 1;
+            snapshot.buffer = buffer_snapshot;
+            snapshot.check_invariants();
+
+            (snapshot.clone(), inlay_edits.into_inner())
+        }
+    }
+
+    pub fn splice(
+        &mut self,
+        to_remove: Vec<InlayId>,
+        to_insert: Vec<Inlay>,
+    ) -> (InlaySnapshot, Vec<InlayEdit>) {
+        let snapshot = &mut self.snapshot;
+        let mut edits = BTreeSet::new();
+
+        self.inlays.retain(|inlay| {
+            let retain = !to_remove.contains(&inlay.id);
+            if !retain {
+                let offset = inlay.position.to_offset(&snapshot.buffer);
+                edits.insert(offset);
+            }
+            retain
+        });
+
+        for inlay_to_insert in to_insert {
+            // Avoid inserting empty inlays.
+            if inlay_to_insert.text.is_empty() {
+                continue;
+            }
+
+            let offset = inlay_to_insert.position.to_offset(&snapshot.buffer);
+            match self.inlays.binary_search_by(|probe| {
+                probe
+                    .position
+                    .cmp(&inlay_to_insert.position, &snapshot.buffer)
+            }) {
+                Ok(ix) | Err(ix) => {
+                    self.inlays.insert(ix, inlay_to_insert);
+                }
+            }
+
+            edits.insert(offset);
+        }
+
+        let buffer_edits = edits
+            .into_iter()
+            .map(|offset| Edit {
+                old: offset..offset,
+                new: offset..offset,
+            })
+            .collect();
+        let buffer_snapshot = snapshot.buffer.clone();
+        let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
+        (snapshot, edits)
+    }
+
+    pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+        self.inlays.iter()
+    }
+
+    #[cfg(test)]
+    pub(crate) fn randomly_mutate(
+        &mut self,
+        next_inlay_id: &mut usize,
+        rng: &mut rand::rngs::StdRng,
+    ) -> (InlaySnapshot, Vec<InlayEdit>) {
+        use rand::prelude::*;
+        use util::post_inc;
+
+        let mut to_remove = Vec::new();
+        let mut to_insert = Vec::new();
+        let snapshot = &mut self.snapshot;
+        for i in 0..rng.gen_range(1..=5) {
+            if self.inlays.is_empty() || rng.gen() {
+                let position = snapshot.buffer.random_byte_range(0, rng).start;
+                let bias = if rng.gen() { Bias::Left } else { Bias::Right };
+                let len = if rng.gen_bool(0.01) {
+                    0
+                } else {
+                    rng.gen_range(1..=5)
+                };
+                let text = util::RandomCharIter::new(&mut *rng)
+                    .filter(|ch| *ch != '\r')
+                    .take(len)
+                    .collect::<String>();
+
+                let inlay_id = if i % 2 == 0 {
+                    InlayId::Hint(post_inc(next_inlay_id))
+                } else {
+                    InlayId::Suggestion(post_inc(next_inlay_id))
+                };
+                log::info!(
+                    "creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
+                    inlay_id,
+                    position,
+                    bias,
+                    text
+                );
+
+                to_insert.push(Inlay {
+                    id: inlay_id,
+                    position: snapshot.buffer.anchor_at(position, bias),
+                    text: text.into(),
+                });
+            } else {
+                to_remove.push(
+                    self.inlays
+                        .iter()
+                        .choose(rng)
+                        .map(|inlay| inlay.id)
+                        .unwrap(),
+                );
+            }
+        }
+        log::info!("removing inlays: {:?}", to_remove);
+
+        let (snapshot, edits) = self.splice(to_remove, to_insert);
+        (snapshot, edits)
+    }
+}
+
+impl InlaySnapshot {
+    pub fn to_point(&self, offset: InlayOffset) -> InlayPoint {
+        let mut cursor = self
+            .transforms
+            .cursor::<(InlayOffset, (InlayPoint, usize))>();
+        cursor.seek(&offset, Bias::Right, &());
+        let overshoot = offset.0 - cursor.start().0 .0;
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let buffer_offset_start = cursor.start().1 .1;
+                let buffer_offset_end = buffer_offset_start + overshoot;
+                let buffer_start = self.buffer.offset_to_point(buffer_offset_start);
+                let buffer_end = self.buffer.offset_to_point(buffer_offset_end);
+                InlayPoint(cursor.start().1 .0 .0 + (buffer_end - buffer_start))
+            }
+            Some(Transform::Inlay(inlay)) => {
+                let overshoot = inlay.text.offset_to_point(overshoot);
+                InlayPoint(cursor.start().1 .0 .0 + overshoot)
+            }
+            None => self.max_point(),
+        }
+    }
+
+    pub fn len(&self) -> InlayOffset {
+        InlayOffset(self.transforms.summary().output.len)
+    }
+
+    pub fn max_point(&self) -> InlayPoint {
+        InlayPoint(self.transforms.summary().output.lines)
+    }
+
+    pub fn to_offset(&self, point: InlayPoint) -> InlayOffset {
+        let mut cursor = self
+            .transforms
+            .cursor::<(InlayPoint, (InlayOffset, Point))>();
+        cursor.seek(&point, Bias::Right, &());
+        let overshoot = point.0 - cursor.start().0 .0;
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let buffer_point_start = cursor.start().1 .1;
+                let buffer_point_end = buffer_point_start + overshoot;
+                let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start);
+                let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end);
+                InlayOffset(cursor.start().1 .0 .0 + (buffer_offset_end - buffer_offset_start))
+            }
+            Some(Transform::Inlay(inlay)) => {
+                let overshoot = inlay.text.point_to_offset(overshoot);
+                InlayOffset(cursor.start().1 .0 .0 + overshoot)
+            }
+            None => self.len(),
+        }
+    }
+
+    pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+        cursor.seek(&point, Bias::Right, &());
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let overshoot = point.0 - cursor.start().0 .0;
+                cursor.start().1 + overshoot
+            }
+            Some(Transform::Inlay(_)) => cursor.start().1,
+            None => self.buffer.max_point(),
+        }
+    }
+
+    pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
+        let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+        cursor.seek(&offset, Bias::Right, &());
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let overshoot = offset - cursor.start().0;
+                cursor.start().1 + overshoot.0
+            }
+            Some(Transform::Inlay(_)) => cursor.start().1,
+            None => self.buffer.len(),
+        }
+    }
+
+    pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset {
+        let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>();
+        cursor.seek(&offset, Bias::Left, &());
+        loop {
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    if offset == cursor.end(&()).0 {
+                        while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+                            if inlay.position.bias() == Bias::Right {
+                                break;
+                            } else {
+                                cursor.next(&());
+                            }
+                        }
+                        return cursor.end(&()).1;
+                    } else {
+                        let overshoot = offset - cursor.start().0;
+                        return InlayOffset(cursor.start().1 .0 + overshoot);
+                    }
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    if inlay.position.bias() == Bias::Left {
+                        cursor.next(&());
+                    } else {
+                        return cursor.start().1;
+                    }
+                }
+                None => {
+                    return self.len();
+                }
+            }
+        }
+    }
+
+    pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
+        let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>();
+        cursor.seek(&point, Bias::Left, &());
+        loop {
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    if point == cursor.end(&()).0 {
+                        while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+                            if inlay.position.bias() == Bias::Right {
+                                break;
+                            } else {
+                                cursor.next(&());
+                            }
+                        }
+                        return cursor.end(&()).1;
+                    } else {
+                        let overshoot = point - cursor.start().0;
+                        return InlayPoint(cursor.start().1 .0 + overshoot);
+                    }
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    if inlay.position.bias() == Bias::Left {
+                        cursor.next(&());
+                    } else {
+                        return cursor.start().1;
+                    }
+                }
+                None => {
+                    return self.max_point();
+                }
+            }
+        }
+    }
+
+    pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+        cursor.seek(&point, Bias::Left, &());
+        loop {
+            match cursor.item() {
+                Some(Transform::Isomorphic(transform)) => {
+                    if cursor.start().0 == point {
+                        if let Some(Transform::Inlay(inlay)) = cursor.prev_item() {
+                            if inlay.position.bias() == Bias::Left {
+                                return point;
+                            } else if bias == Bias::Left {
+                                cursor.prev(&());
+                            } else if transform.first_line_chars == 0 {
+                                point.0 += Point::new(1, 0);
+                            } else {
+                                point.0 += Point::new(0, 1);
+                            }
+                        } else {
+                            return point;
+                        }
+                    } else if cursor.end(&()).0 == point {
+                        if let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+                            if inlay.position.bias() == Bias::Right {
+                                return point;
+                            } else if bias == Bias::Right {
+                                cursor.next(&());
+                            } else if point.0.column == 0 {
+                                point.0.row -= 1;
+                                point.0.column = self.line_len(point.0.row);
+                            } else {
+                                point.0.column -= 1;
+                            }
+                        } else {
+                            return point;
+                        }
+                    } else {
+                        let overshoot = point.0 - cursor.start().0 .0;
+                        let buffer_point = cursor.start().1 + overshoot;
+                        let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias);
+                        let clipped_overshoot = clipped_buffer_point - cursor.start().1;
+                        let clipped_point = InlayPoint(cursor.start().0 .0 + clipped_overshoot);
+                        if clipped_point == point {
+                            return clipped_point;
+                        } else {
+                            point = clipped_point;
+                        }
+                    }
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    if point == cursor.start().0 && inlay.position.bias() == Bias::Right {
+                        match cursor.prev_item() {
+                            Some(Transform::Inlay(inlay)) => {
+                                if inlay.position.bias() == Bias::Left {
+                                    return point;
+                                }
+                            }
+                            _ => return point,
+                        }
+                    } else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left {
+                        match cursor.next_item() {
+                            Some(Transform::Inlay(inlay)) => {
+                                if inlay.position.bias() == Bias::Right {
+                                    return point;
+                                }
+                            }
+                            _ => return point,
+                        }
+                    }
+
+                    if bias == Bias::Left {
+                        point = cursor.start().0;
+                        cursor.prev(&());
+                    } else {
+                        cursor.next(&());
+                        point = cursor.start().0;
+                    }
+                }
+                None => {
+                    bias = bias.invert();
+                    if bias == Bias::Left {
+                        point = cursor.start().0;
+                        cursor.prev(&());
+                    } else {
+                        cursor.next(&());
+                        point = cursor.start().0;
+                    }
+                }
+            }
+        }
+    }
+
+    pub fn text_summary(&self) -> TextSummary {
+        self.transforms.summary().output.clone()
+    }
+
+    pub fn text_summary_for_range(&self, range: Range<InlayOffset>) -> TextSummary {
+        let mut summary = TextSummary::default();
+
+        let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+        cursor.seek(&range.start, Bias::Right, &());
+
+        let overshoot = range.start.0 - cursor.start().0 .0;
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let buffer_start = cursor.start().1;
+                let suffix_start = buffer_start + overshoot;
+                let suffix_end =
+                    buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0);
+                summary = self.buffer.text_summary_for_range(suffix_start..suffix_end);
+                cursor.next(&());
+            }
+            Some(Transform::Inlay(inlay)) => {
+                let suffix_start = overshoot;
+                let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0;
+                summary = inlay.text.cursor(suffix_start).summary(suffix_end);
+                cursor.next(&());
+            }
+            None => {}
+        }
+
+        if range.end > cursor.start().0 {
+            summary += cursor
+                .summary::<_, TransformSummary>(&range.end, Bias::Right, &())
+                .output;
+
+            let overshoot = range.end.0 - cursor.start().0 .0;
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    let prefix_start = cursor.start().1;
+                    let prefix_end = prefix_start + overshoot;
+                    summary += self
+                        .buffer
+                        .text_summary_for_range::<TextSummary, _>(prefix_start..prefix_end);
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    let prefix_end = overshoot;
+                    summary += inlay.text.cursor(0).summary::<TextSummary>(prefix_end);
+                }
+                None => {}
+            }
+        }
+
+        summary
+    }
+
+    pub fn buffer_rows<'a>(&'a self, row: u32) -> InlayBufferRows<'a> {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+        let inlay_point = InlayPoint::new(row, 0);
+        cursor.seek(&inlay_point, Bias::Left, &());
+
+        let max_buffer_row = self.buffer.max_point().row;
+        let mut buffer_point = cursor.start().1;
+        let buffer_row = if row == 0 {
+            0
+        } else {
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    buffer_point += inlay_point.0 - cursor.start().0 .0;
+                    buffer_point.row
+                }
+                _ => cmp::min(buffer_point.row + 1, max_buffer_row),
+            }
+        };
+
+        InlayBufferRows {
+            transforms: cursor,
+            inlay_row: inlay_point.row(),
+            buffer_rows: self.buffer.buffer_rows(buffer_row),
+            max_buffer_row,
+        }
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        let line_start = self.to_offset(InlayPoint::new(row, 0)).0;
+        let line_end = if row >= self.max_point().row() {
+            self.len().0
+        } else {
+            self.to_offset(InlayPoint::new(row + 1, 0)).0 - 1
+        };
+        (line_end - line_start) as u32
+    }
+
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<InlayOffset>,
+        language_aware: bool,
+        highlights: Highlights<'a>,
+    ) -> InlayChunks<'a> {
+        let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+        cursor.seek(&range.start, Bias::Right, &());
+
+        let mut highlight_endpoints = Vec::new();
+        if let Some(text_highlights) = highlights.text_highlights {
+            if !text_highlights.is_empty() {
+                self.apply_text_highlights(
+                    &mut cursor,
+                    &range,
+                    text_highlights,
+                    &mut highlight_endpoints,
+                );
+                cursor.seek(&range.start, Bias::Right, &());
+            }
+        }
+        highlight_endpoints.sort();
+        let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
+        let buffer_chunks = self.buffer.chunks(buffer_range, language_aware);
+
+        InlayChunks {
+            transforms: cursor,
+            buffer_chunks,
+            inlay_chunks: None,
+            inlay_chunk: None,
+            buffer_chunk: None,
+            output_offset: range.start,
+            max_output_offset: range.end,
+            inlay_highlight_style: highlights.inlay_highlight_style,
+            suggestion_highlight_style: highlights.suggestion_highlight_style,
+            highlight_endpoints: highlight_endpoints.into_iter().peekable(),
+            active_highlights: Default::default(),
+            highlights,
+            snapshot: self,
+        }
+    }
+
+    fn apply_text_highlights(
+        &self,
+        cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>,
+        range: &Range<InlayOffset>,
+        text_highlights: &TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
+        highlight_endpoints: &mut Vec<HighlightEndpoint>,
+    ) {
+        while cursor.start().0 < range.end {
+            let transform_start = self
+                .buffer
+                .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0)));
+            let transform_end =
+                {
+                    let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
+                    self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
+                        cursor.end(&()).0,
+                        cursor.start().0 + overshoot,
+                    )))
+                };
+
+            for (tag, text_highlights) in text_highlights.iter() {
+                let style = text_highlights.0;
+                let ranges = &text_highlights.1;
+
+                let start_ix = match ranges.binary_search_by(|probe| {
+                    let cmp = probe.end.cmp(&transform_start, &self.buffer);
+                    if cmp.is_gt() {
+                        cmp::Ordering::Greater
+                    } else {
+                        cmp::Ordering::Less
+                    }
+                }) {
+                    Ok(i) | Err(i) => i,
+                };
+                for range in &ranges[start_ix..] {
+                    if range.start.cmp(&transform_end, &self.buffer).is_ge() {
+                        break;
+                    }
+
+                    highlight_endpoints.push(HighlightEndpoint {
+                        offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)),
+                        is_start: true,
+                        tag: *tag,
+                        style,
+                    });
+                    highlight_endpoints.push(HighlightEndpoint {
+                        offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
+                        is_start: false,
+                        tag: *tag,
+                        style,
+                    });
+                }
+            }
+
+            cursor.next(&());
+        }
+    }
+
+    #[cfg(test)]
+    pub fn text(&self) -> String {
+        self.chunks(Default::default()..self.len(), false, Highlights::default())
+            .map(|chunk| chunk.text)
+            .collect()
+    }
+
+    fn check_invariants(&self) {
+        #[cfg(any(debug_assertions, feature = "test-support"))]
+        {
+            assert_eq!(self.transforms.summary().input, self.buffer.text_summary());
+            let mut transforms = self.transforms.iter().peekable();
+            while let Some(transform) = transforms.next() {
+                let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_));
+                if let Some(next_transform) = transforms.peek() {
+                    let next_transform_is_isomorphic =
+                        matches!(next_transform, Transform::Isomorphic(_));
+                    assert!(
+                        !transform_is_isomorphic || !next_transform_is_isomorphic,
+                        "two adjacent isomorphic transforms"
+                    );
+                }
+            }
+        }
+    }
+}
+
+fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
+    if summary.len == 0 {
+        return;
+    }
+
+    let mut summary = Some(summary);
+    sum_tree.update_last(
+        |transform| {
+            if let Transform::Isomorphic(transform) = transform {
+                *transform += summary.take().unwrap();
+            }
+        },
+        &(),
+    );
+
+    if let Some(summary) = summary {
+        sum_tree.push(Transform::Isomorphic(summary), &());
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::{InlayHighlights, TextHighlights},
+        link_go_to_definition::InlayHighlight,
+        InlayId, MultiBuffer,
+    };
+    use gpui::AppContext;
+    use project::{InlayHint, InlayHintLabel, ResolveState};
+    use rand::prelude::*;
+    use settings::SettingsStore;
+    use std::{cmp::Reverse, env, sync::Arc};
+    use text::Patch;
+    use util::post_inc;
+
+    #[test]
+    fn test_inlay_properties_label_padding() {
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String("a".to_string()),
+                    position: text::Anchor::default(),
+                    padding_left: false,
+                    padding_right: false,
+                    tooltip: None,
+                    kind: None,
+                    resolve_state: ResolveState::Resolved,
+                },
+            )
+            .text
+            .to_string(),
+            "a",
+            "Should not pad label if not requested"
+        );
+
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String("a".to_string()),
+                    position: text::Anchor::default(),
+                    padding_left: true,
+                    padding_right: true,
+                    tooltip: None,
+                    kind: None,
+                    resolve_state: ResolveState::Resolved,
+                },
+            )
+            .text
+            .to_string(),
+            " a ",
+            "Should pad label for every side requested"
+        );
+
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String(" a ".to_string()),
+                    position: text::Anchor::default(),
+                    padding_left: false,
+                    padding_right: false,
+                    tooltip: None,
+                    kind: None,
+                    resolve_state: ResolveState::Resolved,
+                },
+            )
+            .text
+            .to_string(),
+            " a ",
+            "Should not change already padded label"
+        );
+
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String(" a ".to_string()),
+                    position: text::Anchor::default(),
+                    padding_left: true,
+                    padding_right: true,
+                    tooltip: None,
+                    kind: None,
+                    resolve_state: ResolveState::Resolved,
+                },
+            )
+            .text
+            .to_string(),
+            " a ",
+            "Should not change already padded label"
+        );
+    }
+
+    #[gpui::test]
+    fn test_basic_inlays(cx: &mut AppContext) {
+        let buffer = MultiBuffer::build_simple("abcdefghi", cx);
+        let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+        assert_eq!(inlay_snapshot.text(), "abcdefghi");
+        let mut next_inlay_id = 0;
+
+        let (inlay_snapshot, _) = inlay_map.splice(
+            Vec::new(),
+            vec![Inlay {
+                id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                position: buffer.read(cx).snapshot(cx).anchor_after(3),
+                text: "|123|".into(),
+            }],
+        );
+        assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 0)),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 1)),
+            InlayPoint::new(0, 1)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 2)),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 3)),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 4)),
+            InlayPoint::new(0, 9)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 5)),
+            InlayPoint::new(0, 10)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+            InlayPoint::new(0, 9)
+        );
+
+        // Edits before or after the inlay should not affect it.
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx)
+        });
+        let (inlay_snapshot, _) = inlay_map.sync(
+            buffer.read(cx).snapshot(cx),
+            buffer_edits.consume().into_inner(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
+
+        // An edit surrounding the inlay should invalidate it.
+        buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx));
+        let (inlay_snapshot, _) = inlay_map.sync(
+            buffer.read(cx).snapshot(cx),
+            buffer_edits.consume().into_inner(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
+
+        let (inlay_snapshot, _) = inlay_map.splice(
+            Vec::new(),
+            vec![
+                Inlay {
+                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(3),
+                    text: "|123|".into(),
+                },
+                Inlay {
+                    id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_after(3),
+                    text: "|456|".into(),
+                },
+            ],
+        );
+        assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
+
+        // Edits ending where the inlay starts should not move it if it has a left bias.
+        buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx));
+        let (inlay_snapshot, _) = inlay_map.sync(
+            buffer.read(cx).snapshot(cx),
+            buffer_edits.consume().into_inner(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi");
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+            InlayPoint::new(0, 0)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left),
+            InlayPoint::new(0, 1)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right),
+            InlayPoint::new(0, 1)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right),
+            InlayPoint::new(0, 2)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left),
+            InlayPoint::new(0, 8)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left),
+            InlayPoint::new(0, 9)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right),
+            InlayPoint::new(0, 9)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left),
+            InlayPoint::new(0, 10)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right),
+            InlayPoint::new(0, 10)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right),
+            InlayPoint::new(0, 11)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left),
+            InlayPoint::new(0, 17)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left),
+            InlayPoint::new(0, 18)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right),
+            InlayPoint::new(0, 18)
+        );
+
+        // The inlays can be manually removed.
+        let (inlay_snapshot, _) = inlay_map.splice(
+            inlay_map.inlays.iter().map(|inlay| inlay.id).collect(),
+            Vec::new(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
+    }
+
+    #[gpui::test]
+    fn test_inlay_buffer_rows(cx: &mut AppContext) {
+        let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx);
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+        assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi");
+        let mut next_inlay_id = 0;
+
+        let (inlay_snapshot, _) = inlay_map.splice(
+            Vec::new(),
+            vec![
+                Inlay {
+                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(0),
+                    text: "|123|\n".into(),
+                },
+                Inlay {
+                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(4),
+                    text: "|456|".into(),
+                },
+                Inlay {
+                    id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(7),
+                    text: "\n|567|\n".into(),
+                },
+            ],
+        );
+        assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
+        assert_eq!(
+            inlay_snapshot.buffer_rows(0).collect::<Vec<_>>(),
+            vec![Some(0), None, Some(1), None, None, Some(2)]
+        );
+    }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_inlays(cx: &mut AppContext, mut rng: StdRng) {
+        init_test(cx);
+
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let len = rng.gen_range(0..30);
+        let buffer = if rng.gen() {
+            let text = util::RandomCharIter::new(&mut rng)
+                .take(len)
+                .collect::<String>();
+            MultiBuffer::build_simple(&text, cx)
+        } else {
+            MultiBuffer::build_random(&mut rng, cx)
+        };
+        let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let mut next_inlay_id = 0;
+        log::info!("buffer text: {:?}", buffer_snapshot.text());
+        let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        for _ in 0..operations {
+            let mut inlay_edits = Patch::default();
+
+            let mut prev_inlay_text = inlay_snapshot.text();
+            let mut buffer_edits = Vec::new();
+            match rng.gen_range(0..=100) {
+                0..=50 => {
+                    let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+                    log::info!("mutated text: {:?}", snapshot.text());
+                    inlay_edits = Patch::new(edits);
+                }
+                _ => buffer.update(cx, |buffer, cx| {
+                    let subscription = buffer.subscribe();
+                    let edit_count = rng.gen_range(1..=5);
+                    buffer.randomly_mutate(&mut rng, edit_count, cx);
+                    buffer_snapshot = buffer.snapshot(cx);
+                    let edits = subscription.consume().into_inner();
+                    log::info!("editing {:?}", edits);
+                    buffer_edits.extend(edits);
+                }),
+            };
+
+            let (new_inlay_snapshot, new_inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            inlay_snapshot = new_inlay_snapshot;
+            inlay_edits = inlay_edits.compose(new_inlay_edits);
+
+            log::info!("buffer text: {:?}", buffer_snapshot.text());
+            log::info!("inlay text: {:?}", inlay_snapshot.text());
+
+            let inlays = inlay_map
+                .inlays
+                .iter()
+                .filter(|inlay| inlay.position.is_valid(&buffer_snapshot))
+                .map(|inlay| {
+                    let offset = inlay.position.to_offset(&buffer_snapshot);
+                    (offset, inlay.clone())
+                })
+                .collect::<Vec<_>>();
+            let mut expected_text = Rope::from(buffer_snapshot.text());
+            for (offset, inlay) in inlays.iter().rev() {
+                expected_text.replace(*offset..*offset, &inlay.text.to_string());
+            }
+            assert_eq!(inlay_snapshot.text(), expected_text.to_string());
+
+            let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::<Vec<_>>();
+            assert_eq!(
+                expected_buffer_rows.len() as u32,
+                expected_text.max_point().row + 1
+            );
+            for row_start in 0..expected_buffer_rows.len() {
+                assert_eq!(
+                    inlay_snapshot
+                        .buffer_rows(row_start as u32)
+                        .collect::<Vec<_>>(),
+                    &expected_buffer_rows[row_start..],
+                    "incorrect buffer rows starting at {}",
+                    row_start
+                );
+            }
+
+            let mut text_highlights = TextHighlights::default();
+            let text_highlight_count = rng.gen_range(0_usize..10);
+            let mut text_highlight_ranges = (0..text_highlight_count)
+                .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
+                .collect::<Vec<_>>();
+            text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+            log::info!("highlighting text ranges {text_highlight_ranges:?}");
+            text_highlights.insert(
+                Some(TypeId::of::<()>()),
+                Arc::new((
+                    HighlightStyle::default(),
+                    text_highlight_ranges
+                        .into_iter()
+                        .map(|range| {
+                            buffer_snapshot.anchor_before(range.start)
+                                ..buffer_snapshot.anchor_after(range.end)
+                        })
+                        .collect(),
+                )),
+            );
+
+            let mut inlay_highlights = InlayHighlights::default();
+            if !inlays.is_empty() {
+                let inlay_highlight_count = rng.gen_range(0..inlays.len());
+                let mut inlay_indices = BTreeSet::default();
+                while inlay_indices.len() < inlay_highlight_count {
+                    inlay_indices.insert(rng.gen_range(0..inlays.len()));
+                }
+                let new_highlights = inlay_indices
+                    .into_iter()
+                    .filter_map(|i| {
+                        let (_, inlay) = &inlays[i];
+                        let inlay_text_len = inlay.text.len();
+                        match inlay_text_len {
+                            0 => None,
+                            1 => Some(InlayHighlight {
+                                inlay: inlay.id,
+                                inlay_position: inlay.position,
+                                range: 0..1,
+                            }),
+                            n => {
+                                let inlay_text = inlay.text.to_string();
+                                let mut highlight_end = rng.gen_range(1..n);
+                                let mut highlight_start = rng.gen_range(0..highlight_end);
+                                while !inlay_text.is_char_boundary(highlight_end) {
+                                    highlight_end += 1;
+                                }
+                                while !inlay_text.is_char_boundary(highlight_start) {
+                                    highlight_start -= 1;
+                                }
+                                Some(InlayHighlight {
+                                    inlay: inlay.id,
+                                    inlay_position: inlay.position,
+                                    range: highlight_start..highlight_end,
+                                })
+                            }
+                        }
+                    })
+                    .map(|highlight| (highlight.inlay, (HighlightStyle::default(), highlight)))
+                    .collect();
+                log::info!("highlighting inlay ranges {new_highlights:?}");
+                inlay_highlights.insert(TypeId::of::<()>(), new_highlights);
+            }
+
+            for _ in 0..5 {
+                let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
+                end = expected_text.clip_offset(end, Bias::Right);
+                let mut start = rng.gen_range(0..=end);
+                start = expected_text.clip_offset(start, Bias::Right);
+
+                let range = InlayOffset(start)..InlayOffset(end);
+                log::info!("calling inlay_snapshot.chunks({range:?})");
+                let actual_text = inlay_snapshot
+                    .chunks(
+                        range,
+                        false,
+                        Highlights {
+                            text_highlights: Some(&text_highlights),
+                            inlay_highlights: Some(&inlay_highlights),
+                            ..Highlights::default()
+                        },
+                    )
+                    .map(|chunk| chunk.text)
+                    .collect::<String>();
+                assert_eq!(
+                    actual_text,
+                    expected_text.slice(start..end).to_string(),
+                    "incorrect text in range {:?}",
+                    start..end
+                );
+
+                assert_eq!(
+                    inlay_snapshot.text_summary_for_range(InlayOffset(start)..InlayOffset(end)),
+                    expected_text.slice(start..end).summary()
+                );
+            }
+
+            for edit in inlay_edits {
+                prev_inlay_text.replace_range(
+                    edit.new.start.0..edit.new.start.0 + edit.old_len().0,
+                    &inlay_snapshot.text()[edit.new.start.0..edit.new.end.0],
+                );
+            }
+            assert_eq!(prev_inlay_text, inlay_snapshot.text());
+
+            assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0);
+            assert_eq!(expected_text.len(), inlay_snapshot.len().0);
+
+            let mut buffer_point = Point::default();
+            let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+            let mut buffer_chars = buffer_snapshot.chars_at(0);
+            loop {
+                // Ensure conversion from buffer coordinates to inlay coordinates
+                // is consistent.
+                let buffer_offset = buffer_snapshot.point_to_offset(buffer_point);
+                assert_eq!(
+                    inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)),
+                    inlay_point
+                );
+
+                // No matter which bias we clip an inlay point with, it doesn't move
+                // because it was constructed from a buffer point.
+                assert_eq!(
+                    inlay_snapshot.clip_point(inlay_point, Bias::Left),
+                    inlay_point,
+                    "invalid inlay point for buffer point {:?} when clipped left",
+                    buffer_point
+                );
+                assert_eq!(
+                    inlay_snapshot.clip_point(inlay_point, Bias::Right),
+                    inlay_point,
+                    "invalid inlay point for buffer point {:?} when clipped right",
+                    buffer_point
+                );
+
+                if let Some(ch) = buffer_chars.next() {
+                    if ch == '\n' {
+                        buffer_point += Point::new(1, 0);
+                    } else {
+                        buffer_point += Point::new(0, ch.len_utf8() as u32);
+                    }
+
+                    // Ensure that moving forward in the buffer always moves the inlay point forward as well.
+                    let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+                    assert!(new_inlay_point > inlay_point);
+                    inlay_point = new_inlay_point;
+                } else {
+                    break;
+                }
+            }
+
+            let mut inlay_point = InlayPoint::default();
+            let mut inlay_offset = InlayOffset::default();
+            for ch in expected_text.chars() {
+                assert_eq!(
+                    inlay_snapshot.to_offset(inlay_point),
+                    inlay_offset,
+                    "invalid to_offset({:?})",
+                    inlay_point
+                );
+                assert_eq!(
+                    inlay_snapshot.to_point(inlay_offset),
+                    inlay_point,
+                    "invalid to_point({:?})",
+                    inlay_offset
+                );
+
+                let mut bytes = [0; 4];
+                for byte in ch.encode_utf8(&mut bytes).as_bytes() {
+                    inlay_offset.0 += 1;
+                    if *byte == b'\n' {
+                        inlay_point.0 += Point::new(1, 0);
+                    } else {
+                        inlay_point.0 += Point::new(0, 1);
+                    }
+
+                    let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left);
+                    let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right);
+                    assert!(
+                        clipped_left_point <= clipped_right_point,
+                        "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})",
+                        inlay_point,
+                        clipped_left_point,
+                        clipped_right_point
+                    );
+
+                    // Ensure the clipped points are at valid text locations.
+                    assert_eq!(
+                        clipped_left_point.0,
+                        expected_text.clip_point(clipped_left_point.0, Bias::Left)
+                    );
+                    assert_eq!(
+                        clipped_right_point.0,
+                        expected_text.clip_point(clipped_right_point.0, Bias::Right)
+                    );
+
+                    // Ensure the clipped points never overshoot the end of the map.
+                    assert!(clipped_left_point <= inlay_snapshot.max_point());
+                    assert!(clipped_right_point <= inlay_snapshot.max_point());
+
+                    // Ensure the clipped points are at valid buffer locations.
+                    assert_eq!(
+                        inlay_snapshot
+                            .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)),
+                        clipped_left_point,
+                        "to_buffer_point({:?}) = {:?}",
+                        clipped_left_point,
+                        inlay_snapshot.to_buffer_point(clipped_left_point),
+                    );
+                    assert_eq!(
+                        inlay_snapshot
+                            .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)),
+                        clipped_right_point,
+                        "to_buffer_point({:?}) = {:?}",
+                        clipped_right_point,
+                        inlay_snapshot.to_buffer_point(clipped_right_point),
+                    );
+                }
+            }
+        }
+    }
+
+    fn init_test(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+    }
+}

crates/editor2/src/display_map/tab_map.rs ๐Ÿ”—

@@ -0,0 +1,765 @@
+use super::{
+    fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
+    Highlights,
+};
+use crate::MultiBufferSnapshot;
+use language::{Chunk, Point};
+use std::{cmp, mem, num::NonZeroU32, ops::Range};
+use sum_tree::Bias;
+
+const MAX_EXPANSION_COLUMN: u32 = 256;
+
+pub struct TabMap(TabSnapshot);
+
+impl TabMap {
+    pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
+        let snapshot = TabSnapshot {
+            fold_snapshot,
+            tab_size,
+            max_expansion_column: MAX_EXPANSION_COLUMN,
+            version: 0,
+        };
+        (Self(snapshot.clone()), snapshot)
+    }
+
+    #[cfg(test)]
+    pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
+        self.0.max_expansion_column = column;
+        self.0.clone()
+    }
+
+    pub fn sync(
+        &mut self,
+        fold_snapshot: FoldSnapshot,
+        mut fold_edits: Vec<FoldEdit>,
+        tab_size: NonZeroU32,
+    ) -> (TabSnapshot, Vec<TabEdit>) {
+        let old_snapshot = &mut self.0;
+        let mut new_snapshot = TabSnapshot {
+            fold_snapshot,
+            tab_size,
+            max_expansion_column: old_snapshot.max_expansion_column,
+            version: old_snapshot.version,
+        };
+
+        if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
+            new_snapshot.version += 1;
+        }
+
+        let mut tab_edits = Vec::with_capacity(fold_edits.len());
+
+        if old_snapshot.tab_size == new_snapshot.tab_size {
+            // Expand each edit to include the next tab on the same line as the edit,
+            // and any subsequent tabs on that line that moved across the tab expansion
+            // boundary.
+            for fold_edit in &mut fold_edits {
+                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+                let old_end_row_successor_offset = cmp::min(
+                    FoldPoint::new(old_end.row() + 1, 0),
+                    old_snapshot.fold_snapshot.max_point(),
+                )
+                .to_offset(&old_snapshot.fold_snapshot);
+                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
+
+                let mut offset_from_edit = 0;
+                let mut first_tab_offset = None;
+                let mut last_tab_with_changed_expansion_offset = None;
+                'outer: for chunk in old_snapshot.fold_snapshot.chunks(
+                    fold_edit.old.end..old_end_row_successor_offset,
+                    false,
+                    Highlights::default(),
+                ) {
+                    for (ix, _) in chunk.text.match_indices('\t') {
+                        let offset_from_edit = offset_from_edit + (ix as u32);
+                        if first_tab_offset.is_none() {
+                            first_tab_offset = Some(offset_from_edit);
+                        }
+
+                        let old_column = old_end.column() + offset_from_edit;
+                        let new_column = new_end.column() + offset_from_edit;
+                        let was_expanded = old_column < old_snapshot.max_expansion_column;
+                        let is_expanded = new_column < new_snapshot.max_expansion_column;
+                        if was_expanded != is_expanded {
+                            last_tab_with_changed_expansion_offset = Some(offset_from_edit);
+                        } else if !was_expanded && !is_expanded {
+                            break 'outer;
+                        }
+                    }
+
+                    offset_from_edit += chunk.text.len() as u32;
+                    if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
+                        && new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column
+                    {
+                        break;
+                    }
+                }
+
+                if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
+                    fold_edit.old.end.0 += offset as usize + 1;
+                    fold_edit.new.end.0 += offset as usize + 1;
+                }
+            }
+
+            // Combine any edits that overlap due to the expansion.
+            let mut ix = 1;
+            while ix < fold_edits.len() {
+                let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
+                let prev_edit = prev_edits.last_mut().unwrap();
+                let edit = &next_edits[0];
+                if prev_edit.old.end >= edit.old.start {
+                    prev_edit.old.end = edit.old.end;
+                    prev_edit.new.end = edit.new.end;
+                    fold_edits.remove(ix);
+                } else {
+                    ix += 1;
+                }
+            }
+
+            for fold_edit in fold_edits {
+                let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
+                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+                let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
+                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
+                tab_edits.push(TabEdit {
+                    old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
+                    new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
+                });
+            }
+        } else {
+            new_snapshot.version += 1;
+            tab_edits.push(TabEdit {
+                old: TabPoint::zero()..old_snapshot.max_point(),
+                new: TabPoint::zero()..new_snapshot.max_point(),
+            });
+        }
+
+        *old_snapshot = new_snapshot;
+        (old_snapshot.clone(), tab_edits)
+    }
+}
+
+#[derive(Clone)]
+pub struct TabSnapshot {
+    pub fold_snapshot: FoldSnapshot,
+    pub tab_size: NonZeroU32,
+    pub max_expansion_column: u32,
+    pub version: usize,
+}
+
+impl TabSnapshot {
+    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
+        &self.fold_snapshot.inlay_snapshot.buffer
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        let max_point = self.max_point();
+        if row < max_point.row() {
+            self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
+                .0
+                .column
+        } else {
+            max_point.column()
+        }
+    }
+
+    pub fn text_summary(&self) -> TextSummary {
+        self.text_summary_for_range(TabPoint::zero()..self.max_point())
+    }
+
+    pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
+        let input_start = self.to_fold_point(range.start, Bias::Left).0;
+        let input_end = self.to_fold_point(range.end, Bias::Right).0;
+        let input_summary = self
+            .fold_snapshot
+            .text_summary_for_range(input_start..input_end);
+
+        let mut first_line_chars = 0;
+        let line_end = if range.start.row() == range.end.row() {
+            range.end
+        } else {
+            self.max_point()
+        };
+        for c in self
+            .chunks(range.start..line_end, false, Highlights::default())
+            .flat_map(|chunk| chunk.text.chars())
+        {
+            if c == '\n' {
+                break;
+            }
+            first_line_chars += 1;
+        }
+
+        let mut last_line_chars = 0;
+        if range.start.row() == range.end.row() {
+            last_line_chars = first_line_chars;
+        } else {
+            for _ in self
+                .chunks(
+                    TabPoint::new(range.end.row(), 0)..range.end,
+                    false,
+                    Highlights::default(),
+                )
+                .flat_map(|chunk| chunk.text.chars())
+            {
+                last_line_chars += 1;
+            }
+        }
+
+        TextSummary {
+            lines: range.end.0 - range.start.0,
+            first_line_chars,
+            last_line_chars,
+            longest_row: input_summary.longest_row,
+            longest_row_chars: input_summary.longest_row_chars,
+        }
+    }
+
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<TabPoint>,
+        language_aware: bool,
+        highlights: Highlights<'a>,
+    ) -> TabChunks<'a> {
+        let (input_start, expanded_char_column, to_next_stop) =
+            self.to_fold_point(range.start, Bias::Left);
+        let input_column = input_start.column();
+        let input_start = input_start.to_offset(&self.fold_snapshot);
+        let input_end = self
+            .to_fold_point(range.end, Bias::Right)
+            .0
+            .to_offset(&self.fold_snapshot);
+        let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
+            range.end.column() - range.start.column()
+        } else {
+            to_next_stop
+        };
+
+        TabChunks {
+            fold_chunks: self.fold_snapshot.chunks(
+                input_start..input_end,
+                language_aware,
+                highlights,
+            ),
+            input_column,
+            column: expanded_char_column,
+            max_expansion_column: self.max_expansion_column,
+            output_position: range.start.0,
+            max_output_position: range.end.0,
+            tab_size: self.tab_size,
+            chunk: Chunk {
+                text: &SPACES[0..(to_next_stop as usize)],
+                is_tab: true,
+                ..Default::default()
+            },
+            inside_leading_tab: to_next_stop > 0,
+        }
+    }
+
+    pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
+        self.fold_snapshot.buffer_rows(row)
+    }
+
+    #[cfg(test)]
+    pub fn text(&self) -> String {
+        self.chunks(
+            TabPoint::zero()..self.max_point(),
+            false,
+            Highlights::default(),
+        )
+        .map(|chunk| chunk.text)
+        .collect()
+    }
+
+    pub fn max_point(&self) -> TabPoint {
+        self.to_tab_point(self.fold_snapshot.max_point())
+    }
+
+    pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
+        self.to_tab_point(
+            self.fold_snapshot
+                .clip_point(self.to_fold_point(point, bias).0, bias),
+        )
+    }
+
+    pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
+        let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
+        let expanded = self.expand_tabs(chars, input.column());
+        TabPoint::new(input.row(), expanded)
+    }
+
+    pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
+        let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
+        let expanded = output.column();
+        let (collapsed, expanded_char_column, to_next_stop) =
+            self.collapse_tabs(chars, expanded, bias);
+        (
+            FoldPoint::new(output.row(), collapsed as u32),
+            expanded_char_column,
+            to_next_stop,
+        )
+    }
+
+    pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
+        let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
+        let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+        self.to_tab_point(fold_point)
+    }
+
+    pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
+        let fold_point = self.to_fold_point(point, bias).0;
+        let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+        self.fold_snapshot
+            .inlay_snapshot
+            .to_buffer_point(inlay_point)
+    }
+
+    fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
+        let tab_size = self.tab_size.get();
+
+        let mut expanded_chars = 0;
+        let mut expanded_bytes = 0;
+        let mut collapsed_bytes = 0;
+        let end_column = column.min(self.max_expansion_column);
+        for c in chars {
+            if collapsed_bytes >= end_column {
+                break;
+            }
+            if c == '\t' {
+                let tab_len = tab_size - expanded_chars % tab_size;
+                expanded_bytes += tab_len;
+                expanded_chars += tab_len;
+            } else {
+                expanded_bytes += c.len_utf8() as u32;
+                expanded_chars += 1;
+            }
+            collapsed_bytes += c.len_utf8() as u32;
+        }
+        expanded_bytes + column.saturating_sub(collapsed_bytes)
+    }
+
+    fn collapse_tabs(
+        &self,
+        chars: impl Iterator<Item = char>,
+        column: u32,
+        bias: Bias,
+    ) -> (u32, u32, u32) {
+        let tab_size = self.tab_size.get();
+
+        let mut expanded_bytes = 0;
+        let mut expanded_chars = 0;
+        let mut collapsed_bytes = 0;
+        for c in chars {
+            if expanded_bytes >= column {
+                break;
+            }
+            if collapsed_bytes >= self.max_expansion_column {
+                break;
+            }
+
+            if c == '\t' {
+                let tab_len = tab_size - (expanded_chars % tab_size);
+                expanded_chars += tab_len;
+                expanded_bytes += tab_len;
+                if expanded_bytes > column {
+                    expanded_chars -= expanded_bytes - column;
+                    return match bias {
+                        Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
+                        Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
+                    };
+                }
+            } else {
+                expanded_chars += 1;
+                expanded_bytes += c.len_utf8() as u32;
+            }
+
+            if expanded_bytes > column && matches!(bias, Bias::Left) {
+                expanded_chars -= 1;
+                break;
+            }
+
+            collapsed_bytes += c.len_utf8() as u32;
+        }
+        (
+            collapsed_bytes + column.saturating_sub(expanded_bytes),
+            expanded_chars,
+            0,
+        )
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct TabPoint(pub Point);
+
+impl TabPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(Point::new(row, column))
+    }
+
+    pub fn zero() -> Self {
+        Self::new(0, 0)
+    }
+
+    pub fn row(self) -> u32 {
+        self.0.row
+    }
+
+    pub fn column(self) -> u32 {
+        self.0.column
+    }
+}
+
+impl From<Point> for TabPoint {
+    fn from(point: Point) -> Self {
+        Self(point)
+    }
+}
+
+pub type TabEdit = text::Edit<TabPoint>;
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct TextSummary {
+    pub lines: Point,
+    pub first_line_chars: u32,
+    pub last_line_chars: u32,
+    pub longest_row: u32,
+    pub longest_row_chars: u32,
+}
+
+impl<'a> From<&'a str> for TextSummary {
+    fn from(text: &'a str) -> Self {
+        let sum = text::TextSummary::from(text);
+
+        TextSummary {
+            lines: sum.lines,
+            first_line_chars: sum.first_line_chars,
+            last_line_chars: sum.last_line_chars,
+            longest_row: sum.longest_row,
+            longest_row_chars: sum.longest_row_chars,
+        }
+    }
+}
+
+impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
+    fn add_assign(&mut self, other: &'a Self) {
+        let joined_chars = self.last_line_chars + other.first_line_chars;
+        if joined_chars > self.longest_row_chars {
+            self.longest_row = self.lines.row;
+            self.longest_row_chars = joined_chars;
+        }
+        if other.longest_row_chars > self.longest_row_chars {
+            self.longest_row = self.lines.row + other.longest_row;
+            self.longest_row_chars = other.longest_row_chars;
+        }
+
+        if self.lines.row == 0 {
+            self.first_line_chars += other.first_line_chars;
+        }
+
+        if other.lines.row == 0 {
+            self.last_line_chars += other.first_line_chars;
+        } else {
+            self.last_line_chars = other.last_line_chars;
+        }
+
+        self.lines += &other.lines;
+    }
+}
+
+// Handles a tab width <= 16
+const SPACES: &str = "                ";
+
+pub struct TabChunks<'a> {
+    fold_chunks: FoldChunks<'a>,
+    chunk: Chunk<'a>,
+    column: u32,
+    max_expansion_column: u32,
+    output_position: Point,
+    input_column: u32,
+    max_output_position: Point,
+    tab_size: NonZeroU32,
+    inside_leading_tab: bool,
+}
+
+impl<'a> Iterator for TabChunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.chunk.text.is_empty() {
+            if let Some(chunk) = self.fold_chunks.next() {
+                self.chunk = chunk;
+                if self.inside_leading_tab {
+                    self.chunk.text = &self.chunk.text[1..];
+                    self.inside_leading_tab = false;
+                    self.input_column += 1;
+                }
+            } else {
+                return None;
+            }
+        }
+
+        for (ix, c) in self.chunk.text.char_indices() {
+            match c {
+                '\t' => {
+                    if ix > 0 {
+                        let (prefix, suffix) = self.chunk.text.split_at(ix);
+                        self.chunk.text = suffix;
+                        return Some(Chunk {
+                            text: prefix,
+                            ..self.chunk
+                        });
+                    } else {
+                        self.chunk.text = &self.chunk.text[1..];
+                        let tab_size = if self.input_column < self.max_expansion_column {
+                            self.tab_size.get() as u32
+                        } else {
+                            1
+                        };
+                        let mut len = tab_size - self.column % tab_size;
+                        let next_output_position = cmp::min(
+                            self.output_position + Point::new(0, len),
+                            self.max_output_position,
+                        );
+                        len = next_output_position.column - self.output_position.column;
+                        self.column += len;
+                        self.input_column += 1;
+                        self.output_position = next_output_position;
+                        return Some(Chunk {
+                            text: &SPACES[..len as usize],
+                            is_tab: true,
+                            ..self.chunk
+                        });
+                    }
+                }
+                '\n' => {
+                    self.column = 0;
+                    self.input_column = 0;
+                    self.output_position += Point::new(1, 0);
+                }
+                _ => {
+                    self.column += 1;
+                    if !self.inside_leading_tab {
+                        self.input_column += c.len_utf8() as u32;
+                    }
+                    self.output_position.column += c.len_utf8() as u32;
+                }
+            }
+        }
+
+        Some(mem::take(&mut self.chunk))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::{fold_map::FoldMap, inlay_map::InlayMap},
+        MultiBuffer,
+    };
+    use rand::{prelude::StdRng, Rng};
+
+    #[gpui::test]
+    fn test_expand_tabs(cx: &mut gpui::AppContext) {
+        let buffer = MultiBuffer::build_simple("", cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+
+        assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
+        assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
+        assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
+    }
+
+    #[gpui::test]
+    fn test_long_lines(cx: &mut gpui::AppContext) {
+        let max_expansion_column = 12;
+        let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
+        let output = "A   BC  DEF G   HI J K L M";
+
+        let buffer = MultiBuffer::build_simple(input, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+
+        tab_snapshot.max_expansion_column = max_expansion_column;
+        assert_eq!(tab_snapshot.text(), output);
+
+        for (ix, c) in input.char_indices() {
+            assert_eq!(
+                tab_snapshot
+                    .chunks(
+                        TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
+                        false,
+                        Highlights::default(),
+                    )
+                    .map(|c| c.text)
+                    .collect::<String>(),
+                &output[ix..],
+                "text from index {ix}"
+            );
+
+            if c != '\t' {
+                let input_point = Point::new(0, ix as u32);
+                let output_point = Point::new(0, output.find(c).unwrap() as u32);
+                assert_eq!(
+                    tab_snapshot.to_tab_point(FoldPoint(input_point)),
+                    TabPoint(output_point),
+                    "to_tab_point({input_point:?})"
+                );
+                assert_eq!(
+                    tab_snapshot
+                        .to_fold_point(TabPoint(output_point), Bias::Left)
+                        .0,
+                    FoldPoint(input_point),
+                    "to_fold_point({output_point:?})"
+                );
+            }
+        }
+    }
+
+    #[gpui::test]
+    fn test_long_lines_with_character_spanning_max_expansion_column(cx: &mut gpui::AppContext) {
+        let max_expansion_column = 8;
+        let input = "abcdefgโ‹ฏhij";
+
+        let buffer = MultiBuffer::build_simple(input, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+
+        tab_snapshot.max_expansion_column = max_expansion_column;
+        assert_eq!(tab_snapshot.text(), input);
+    }
+
+    #[gpui::test]
+    fn test_marking_tabs(cx: &mut gpui::AppContext) {
+        let input = "\t \thello";
+
+        let buffer = MultiBuffer::build_simple(&input, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+
+        assert_eq!(
+            chunks(&tab_snapshot, TabPoint::zero()),
+            vec![
+                ("    ".to_string(), true),
+                (" ".to_string(), false),
+                ("   ".to_string(), true),
+                ("hello".to_string(), false),
+            ]
+        );
+        assert_eq!(
+            chunks(&tab_snapshot, TabPoint::new(0, 2)),
+            vec![
+                ("  ".to_string(), true),
+                (" ".to_string(), false),
+                ("   ".to_string(), true),
+                ("hello".to_string(), false),
+            ]
+        );
+
+        fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
+            let mut chunks = Vec::new();
+            let mut was_tab = false;
+            let mut text = String::new();
+            for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default())
+            {
+                if chunk.is_tab != was_tab {
+                    if !text.is_empty() {
+                        chunks.push((mem::take(&mut text), was_tab));
+                    }
+                    was_tab = chunk.is_tab;
+                }
+                text.push_str(chunk.text);
+            }
+
+            if !text.is_empty() {
+                chunks.push((text, was_tab));
+            }
+            chunks
+        }
+    }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) {
+        let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
+        let len = rng.gen_range(0..30);
+        let buffer = if rng.gen() {
+            let text = util::RandomCharIter::new(&mut rng)
+                .take(len)
+                .collect::<String>();
+            MultiBuffer::build_simple(&text, cx)
+        } else {
+            MultiBuffer::build_random(&mut rng, cx)
+        };
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        log::info!("Buffer text: {:?}", buffer_snapshot.text());
+
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+        let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
+        fold_map.randomly_mutate(&mut rng);
+        let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
+        log::info!("FoldMap text: {:?}", fold_snapshot.text());
+        let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
+        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+
+        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
+        let tabs_snapshot = tab_map.set_max_expansion_column(32);
+
+        let text = text::Rope::from(tabs_snapshot.text().as_str());
+        log::info!(
+            "TabMap text (tab size: {}): {:?}",
+            tab_size,
+            tabs_snapshot.text(),
+        );
+
+        for _ in 0..5 {
+            let end_row = rng.gen_range(0..=text.max_point().row);
+            let end_column = rng.gen_range(0..=text.line_len(end_row));
+            let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
+            let start_row = rng.gen_range(0..=text.max_point().row);
+            let start_column = rng.gen_range(0..=text.line_len(start_row));
+            let mut start =
+                TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
+            if start > end {
+                mem::swap(&mut start, &mut end);
+            }
+
+            let expected_text = text
+                .chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
+                .collect::<String>();
+            let expected_summary = TextSummary::from(expected_text.as_str());
+            assert_eq!(
+                tabs_snapshot
+                    .chunks(start..end, false, Highlights::default())
+                    .map(|c| c.text)
+                    .collect::<String>(),
+                expected_text,
+                "chunks({:?}..{:?})",
+                start,
+                end
+            );
+
+            let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
+            if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
+                actual_summary.longest_row = expected_summary.longest_row;
+                actual_summary.longest_row_chars = expected_summary.longest_row_chars;
+            }
+            assert_eq!(actual_summary, expected_summary);
+        }
+
+        for row in 0..=text.max_point().row {
+            assert_eq!(
+                tabs_snapshot.line_len(row),
+                text.line_len(row),
+                "line_len({row})"
+            );
+        }
+    }
+}

crates/editor2/src/display_map/wrap_map.rs ๐Ÿ”—

@@ -0,0 +1,1355 @@
+use super::{
+    fold_map::FoldBufferRows,
+    tab_map::{self, TabEdit, TabPoint, TabSnapshot},
+    Highlights,
+};
+use crate::MultiBufferSnapshot;
+use gpui::{
+    fonts::FontId, text_layout::LineWrapper, AppContext, Entity, ModelContext, ModelHandle, Task,
+};
+use language::{Chunk, Point};
+use lazy_static::lazy_static;
+use smol::future::yield_now;
+use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
+use sum_tree::{Bias, Cursor, SumTree};
+use text::Patch;
+
+pub use super::tab_map::TextSummary;
+pub type WrapEdit = text::Edit<u32>;
+
+pub struct WrapMap {
+    snapshot: WrapSnapshot,
+    pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
+    interpolated_edits: Patch<u32>,
+    edits_since_sync: Patch<u32>,
+    wrap_width: Option<f32>,
+    background_task: Option<Task<()>>,
+    font: (FontId, f32),
+}
+
+impl Entity for WrapMap {
+    type Event = ();
+}
+
+#[derive(Clone)]
+pub struct WrapSnapshot {
+    tab_snapshot: TabSnapshot,
+    transforms: SumTree<Transform>,
+    interpolated: bool,
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+struct Transform {
+    summary: TransformSummary,
+    display_text: Option<&'static str>,
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+struct TransformSummary {
+    input: TextSummary,
+    output: TextSummary,
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct WrapPoint(pub Point);
+
+pub struct WrapChunks<'a> {
+    input_chunks: tab_map::TabChunks<'a>,
+    input_chunk: Chunk<'a>,
+    output_position: WrapPoint,
+    max_output_row: u32,
+    transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
+}
+
+#[derive(Clone)]
+pub struct WrapBufferRows<'a> {
+    input_buffer_rows: FoldBufferRows<'a>,
+    input_buffer_row: Option<u32>,
+    output_row: u32,
+    soft_wrapped: bool,
+    max_output_row: u32,
+    transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
+}
+
+impl WrapMap {
+    pub fn new(
+        tab_snapshot: TabSnapshot,
+        font_id: FontId,
+        font_size: f32,
+        wrap_width: Option<f32>,
+        cx: &mut AppContext,
+    ) -> (ModelHandle<Self>, WrapSnapshot) {
+        let handle = cx.add_model(|cx| {
+            let mut this = Self {
+                font: (font_id, font_size),
+                wrap_width: None,
+                pending_edits: Default::default(),
+                interpolated_edits: Default::default(),
+                edits_since_sync: Default::default(),
+                snapshot: WrapSnapshot::new(tab_snapshot),
+                background_task: None,
+            };
+            this.set_wrap_width(wrap_width, cx);
+            mem::take(&mut this.edits_since_sync);
+            this
+        });
+        let snapshot = handle.read(cx).snapshot.clone();
+        (handle, snapshot)
+    }
+
+    #[cfg(test)]
+    pub fn is_rewrapping(&self) -> bool {
+        self.background_task.is_some()
+    }
+
+    pub fn sync(
+        &mut self,
+        tab_snapshot: TabSnapshot,
+        edits: Vec<TabEdit>,
+        cx: &mut ModelContext<Self>,
+    ) -> (WrapSnapshot, Patch<u32>) {
+        if self.wrap_width.is_some() {
+            self.pending_edits.push_back((tab_snapshot, edits));
+            self.flush_edits(cx);
+        } else {
+            self.edits_since_sync = self
+                .edits_since_sync
+                .compose(&self.snapshot.interpolate(tab_snapshot, &edits));
+            self.snapshot.interpolated = false;
+        }
+
+        (self.snapshot.clone(), mem::take(&mut self.edits_since_sync))
+    }
+
+    pub fn set_font(
+        &mut self,
+        font_id: FontId,
+        font_size: f32,
+        cx: &mut ModelContext<Self>,
+    ) -> bool {
+        if (font_id, font_size) != self.font {
+            self.font = (font_id, font_size);
+            self.rewrap(cx);
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn set_wrap_width(&mut self, wrap_width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
+        if wrap_width == self.wrap_width {
+            return false;
+        }
+
+        self.wrap_width = wrap_width;
+        self.rewrap(cx);
+        true
+    }
+
+    fn rewrap(&mut self, cx: &mut ModelContext<Self>) {
+        self.background_task.take();
+        self.interpolated_edits.clear();
+        self.pending_edits.clear();
+
+        if let Some(wrap_width) = self.wrap_width {
+            let mut new_snapshot = self.snapshot.clone();
+            let font_cache = cx.font_cache().clone();
+            let (font_id, font_size) = self.font;
+            let task = cx.background().spawn(async move {
+                let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
+                let tab_snapshot = new_snapshot.tab_snapshot.clone();
+                let range = TabPoint::zero()..tab_snapshot.max_point();
+                let edits = new_snapshot
+                    .update(
+                        tab_snapshot,
+                        &[TabEdit {
+                            old: range.clone(),
+                            new: range.clone(),
+                        }],
+                        wrap_width,
+                        &mut line_wrapper,
+                    )
+                    .await;
+                (new_snapshot, edits)
+            });
+
+            match cx
+                .background()
+                .block_with_timeout(Duration::from_millis(5), task)
+            {
+                Ok((snapshot, edits)) => {
+                    self.snapshot = snapshot;
+                    self.edits_since_sync = self.edits_since_sync.compose(&edits);
+                    cx.notify();
+                }
+                Err(wrap_task) => {
+                    self.background_task = Some(cx.spawn(|this, mut cx| async move {
+                        let (snapshot, edits) = wrap_task.await;
+                        this.update(&mut cx, |this, cx| {
+                            this.snapshot = snapshot;
+                            this.edits_since_sync = this
+                                .edits_since_sync
+                                .compose(mem::take(&mut this.interpolated_edits).invert())
+                                .compose(&edits);
+                            this.background_task = None;
+                            this.flush_edits(cx);
+                            cx.notify();
+                        });
+                    }));
+                }
+            }
+        } else {
+            let old_rows = self.snapshot.transforms.summary().output.lines.row + 1;
+            self.snapshot.transforms = SumTree::new();
+            let summary = self.snapshot.tab_snapshot.text_summary();
+            if !summary.lines.is_zero() {
+                self.snapshot
+                    .transforms
+                    .push(Transform::isomorphic(summary), &());
+            }
+            let new_rows = self.snapshot.transforms.summary().output.lines.row + 1;
+            self.snapshot.interpolated = false;
+            self.edits_since_sync = self.edits_since_sync.compose(&Patch::new(vec![WrapEdit {
+                old: 0..old_rows,
+                new: 0..new_rows,
+            }]));
+        }
+    }
+
+    fn flush_edits(&mut self, cx: &mut ModelContext<Self>) {
+        if !self.snapshot.interpolated {
+            let mut to_remove_len = 0;
+            for (tab_snapshot, _) in &self.pending_edits {
+                if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
+                    to_remove_len += 1;
+                } else {
+                    break;
+                }
+            }
+            self.pending_edits.drain(..to_remove_len);
+        }
+
+        if self.pending_edits.is_empty() {
+            return;
+        }
+
+        if let Some(wrap_width) = self.wrap_width {
+            if self.background_task.is_none() {
+                let pending_edits = self.pending_edits.clone();
+                let mut snapshot = self.snapshot.clone();
+                let font_cache = cx.font_cache().clone();
+                let (font_id, font_size) = self.font;
+                let update_task = cx.background().spawn(async move {
+                    let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
+
+                    let mut edits = Patch::default();
+                    for (tab_snapshot, tab_edits) in pending_edits {
+                        let wrap_edits = snapshot
+                            .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
+                            .await;
+                        edits = edits.compose(&wrap_edits);
+                    }
+                    (snapshot, edits)
+                });
+
+                match cx
+                    .background()
+                    .block_with_timeout(Duration::from_millis(1), update_task)
+                {
+                    Ok((snapshot, output_edits)) => {
+                        self.snapshot = snapshot;
+                        self.edits_since_sync = self.edits_since_sync.compose(&output_edits);
+                    }
+                    Err(update_task) => {
+                        self.background_task = Some(cx.spawn(|this, mut cx| async move {
+                            let (snapshot, edits) = update_task.await;
+                            this.update(&mut cx, |this, cx| {
+                                this.snapshot = snapshot;
+                                this.edits_since_sync = this
+                                    .edits_since_sync
+                                    .compose(mem::take(&mut this.interpolated_edits).invert())
+                                    .compose(&edits);
+                                this.background_task = None;
+                                this.flush_edits(cx);
+                                cx.notify();
+                            });
+                        }));
+                    }
+                }
+            }
+        }
+
+        let was_interpolated = self.snapshot.interpolated;
+        let mut to_remove_len = 0;
+        for (tab_snapshot, edits) in &self.pending_edits {
+            if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
+                to_remove_len += 1;
+            } else {
+                let interpolated_edits = self.snapshot.interpolate(tab_snapshot.clone(), edits);
+                self.edits_since_sync = self.edits_since_sync.compose(&interpolated_edits);
+                self.interpolated_edits = self.interpolated_edits.compose(&interpolated_edits);
+            }
+        }
+
+        if !was_interpolated {
+            self.pending_edits.drain(..to_remove_len);
+        }
+    }
+}
+
+impl WrapSnapshot {
+    fn new(tab_snapshot: TabSnapshot) -> Self {
+        let mut transforms = SumTree::new();
+        let extent = tab_snapshot.text_summary();
+        if !extent.lines.is_zero() {
+            transforms.push(Transform::isomorphic(extent), &());
+        }
+        Self {
+            transforms,
+            tab_snapshot,
+            interpolated: true,
+        }
+    }
+
+    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
+        self.tab_snapshot.buffer_snapshot()
+    }
+
+    fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch<u32> {
+        let mut new_transforms;
+        if tab_edits.is_empty() {
+            new_transforms = self.transforms.clone();
+        } else {
+            let mut old_cursor = self.transforms.cursor::<TabPoint>();
+
+            let mut tab_edits_iter = tab_edits.iter().peekable();
+            new_transforms =
+                old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &());
+
+            while let Some(edit) = tab_edits_iter.next() {
+                if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) {
+                    let summary = new_tab_snapshot.text_summary_for_range(
+                        TabPoint::from(new_transforms.summary().input.lines)..edit.new.start,
+                    );
+                    new_transforms.push_or_extend(Transform::isomorphic(summary));
+                }
+
+                if !edit.new.is_empty() {
+                    new_transforms.push_or_extend(Transform::isomorphic(
+                        new_tab_snapshot.text_summary_for_range(edit.new.clone()),
+                    ));
+                }
+
+                old_cursor.seek_forward(&edit.old.end, Bias::Right, &());
+                if let Some(next_edit) = tab_edits_iter.peek() {
+                    if next_edit.old.start > old_cursor.end(&()) {
+                        if old_cursor.end(&()) > edit.old.end {
+                            let summary = self
+                                .tab_snapshot
+                                .text_summary_for_range(edit.old.end..old_cursor.end(&()));
+                            new_transforms.push_or_extend(Transform::isomorphic(summary));
+                        }
+
+                        old_cursor.next(&());
+                        new_transforms.append(
+                            old_cursor.slice(&next_edit.old.start, Bias::Right, &()),
+                            &(),
+                        );
+                    }
+                } else {
+                    if old_cursor.end(&()) > edit.old.end {
+                        let summary = self
+                            .tab_snapshot
+                            .text_summary_for_range(edit.old.end..old_cursor.end(&()));
+                        new_transforms.push_or_extend(Transform::isomorphic(summary));
+                    }
+                    old_cursor.next(&());
+                    new_transforms.append(old_cursor.suffix(&()), &());
+                }
+            }
+        }
+
+        let old_snapshot = mem::replace(
+            self,
+            WrapSnapshot {
+                tab_snapshot: new_tab_snapshot,
+                transforms: new_transforms,
+                interpolated: true,
+            },
+        );
+        self.check_invariants();
+        old_snapshot.compute_edits(tab_edits, self)
+    }
+
+    async fn update(
+        &mut self,
+        new_tab_snapshot: TabSnapshot,
+        tab_edits: &[TabEdit],
+        wrap_width: f32,
+        line_wrapper: &mut LineWrapper,
+    ) -> Patch<u32> {
+        #[derive(Debug)]
+        struct RowEdit {
+            old_rows: Range<u32>,
+            new_rows: Range<u32>,
+        }
+
+        let mut tab_edits_iter = tab_edits.iter().peekable();
+        let mut row_edits = Vec::new();
+        while let Some(edit) = tab_edits_iter.next() {
+            let mut row_edit = RowEdit {
+                old_rows: edit.old.start.row()..edit.old.end.row() + 1,
+                new_rows: edit.new.start.row()..edit.new.end.row() + 1,
+            };
+
+            while let Some(next_edit) = tab_edits_iter.peek() {
+                if next_edit.old.start.row() <= row_edit.old_rows.end {
+                    row_edit.old_rows.end = next_edit.old.end.row() + 1;
+                    row_edit.new_rows.end = next_edit.new.end.row() + 1;
+                    tab_edits_iter.next();
+                } else {
+                    break;
+                }
+            }
+
+            row_edits.push(row_edit);
+        }
+
+        let mut new_transforms;
+        if row_edits.is_empty() {
+            new_transforms = self.transforms.clone();
+        } else {
+            let mut row_edits = row_edits.into_iter().peekable();
+            let mut old_cursor = self.transforms.cursor::<TabPoint>();
+
+            new_transforms = old_cursor.slice(
+                &TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
+                Bias::Right,
+                &(),
+            );
+
+            while let Some(edit) = row_edits.next() {
+                if edit.new_rows.start > new_transforms.summary().input.lines.row {
+                    let summary = new_tab_snapshot.text_summary_for_range(
+                        TabPoint(new_transforms.summary().input.lines)
+                            ..TabPoint::new(edit.new_rows.start, 0),
+                    );
+                    new_transforms.push_or_extend(Transform::isomorphic(summary));
+                }
+
+                let mut line = String::new();
+                let mut remaining = None;
+                let mut chunks = new_tab_snapshot.chunks(
+                    TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
+                    false,
+                    Highlights::default(),
+                );
+                let mut edit_transforms = Vec::<Transform>::new();
+                for _ in edit.new_rows.start..edit.new_rows.end {
+                    while let Some(chunk) =
+                        remaining.take().or_else(|| chunks.next().map(|c| c.text))
+                    {
+                        if let Some(ix) = chunk.find('\n') {
+                            line.push_str(&chunk[..ix + 1]);
+                            remaining = Some(&chunk[ix + 1..]);
+                            break;
+                        } else {
+                            line.push_str(chunk)
+                        }
+                    }
+
+                    if line.is_empty() {
+                        break;
+                    }
+
+                    let mut prev_boundary_ix = 0;
+                    for boundary in line_wrapper.wrap_line(&line, wrap_width) {
+                        let wrapped = &line[prev_boundary_ix..boundary.ix];
+                        push_isomorphic(&mut edit_transforms, TextSummary::from(wrapped));
+                        edit_transforms.push(Transform::wrap(boundary.next_indent));
+                        prev_boundary_ix = boundary.ix;
+                    }
+
+                    if prev_boundary_ix < line.len() {
+                        push_isomorphic(
+                            &mut edit_transforms,
+                            TextSummary::from(&line[prev_boundary_ix..]),
+                        );
+                    }
+
+                    line.clear();
+                    yield_now().await;
+                }
+
+                let mut edit_transforms = edit_transforms.into_iter();
+                if let Some(transform) = edit_transforms.next() {
+                    new_transforms.push_or_extend(transform);
+                }
+                new_transforms.extend(edit_transforms, &());
+
+                old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right, &());
+                if let Some(next_edit) = row_edits.peek() {
+                    if next_edit.old_rows.start > old_cursor.end(&()).row() {
+                        if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
+                            let summary = self.tab_snapshot.text_summary_for_range(
+                                TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
+                            );
+                            new_transforms.push_or_extend(Transform::isomorphic(summary));
+                        }
+                        old_cursor.next(&());
+                        new_transforms.append(
+                            old_cursor.slice(
+                                &TabPoint::new(next_edit.old_rows.start, 0),
+                                Bias::Right,
+                                &(),
+                            ),
+                            &(),
+                        );
+                    }
+                } else {
+                    if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
+                        let summary = self.tab_snapshot.text_summary_for_range(
+                            TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
+                        );
+                        new_transforms.push_or_extend(Transform::isomorphic(summary));
+                    }
+                    old_cursor.next(&());
+                    new_transforms.append(old_cursor.suffix(&()), &());
+                }
+            }
+        }
+
+        let old_snapshot = mem::replace(
+            self,
+            WrapSnapshot {
+                tab_snapshot: new_tab_snapshot,
+                transforms: new_transforms,
+                interpolated: false,
+            },
+        );
+        self.check_invariants();
+        old_snapshot.compute_edits(tab_edits, self)
+    }
+
+    fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &WrapSnapshot) -> Patch<u32> {
+        let mut wrap_edits = Vec::new();
+        let mut old_cursor = self.transforms.cursor::<TransformSummary>();
+        let mut new_cursor = new_snapshot.transforms.cursor::<TransformSummary>();
+        for mut tab_edit in tab_edits.iter().cloned() {
+            tab_edit.old.start.0.column = 0;
+            tab_edit.old.end.0 += Point::new(1, 0);
+            tab_edit.new.start.0.column = 0;
+            tab_edit.new.end.0 += Point::new(1, 0);
+
+            old_cursor.seek(&tab_edit.old.start, Bias::Right, &());
+            let mut old_start = old_cursor.start().output.lines;
+            old_start += tab_edit.old.start.0 - old_cursor.start().input.lines;
+
+            old_cursor.seek(&tab_edit.old.end, Bias::Right, &());
+            let mut old_end = old_cursor.start().output.lines;
+            old_end += tab_edit.old.end.0 - old_cursor.start().input.lines;
+
+            new_cursor.seek(&tab_edit.new.start, Bias::Right, &());
+            let mut new_start = new_cursor.start().output.lines;
+            new_start += tab_edit.new.start.0 - new_cursor.start().input.lines;
+
+            new_cursor.seek(&tab_edit.new.end, Bias::Right, &());
+            let mut new_end = new_cursor.start().output.lines;
+            new_end += tab_edit.new.end.0 - new_cursor.start().input.lines;
+
+            wrap_edits.push(WrapEdit {
+                old: old_start.row..old_end.row,
+                new: new_start.row..new_end.row,
+            });
+        }
+
+        consolidate_wrap_edits(&mut wrap_edits);
+        Patch::new(wrap_edits)
+    }
+
+    pub fn chunks<'a>(
+        &'a self,
+        rows: Range<u32>,
+        language_aware: bool,
+        highlights: Highlights<'a>,
+    ) -> WrapChunks<'a> {
+        let output_start = WrapPoint::new(rows.start, 0);
+        let output_end = WrapPoint::new(rows.end, 0);
+        let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+        transforms.seek(&output_start, Bias::Right, &());
+        let mut input_start = TabPoint(transforms.start().1 .0);
+        if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+            input_start.0 += output_start.0 - transforms.start().0 .0;
+        }
+        let input_end = self
+            .to_tab_point(output_end)
+            .min(self.tab_snapshot.max_point());
+        WrapChunks {
+            input_chunks: self.tab_snapshot.chunks(
+                input_start..input_end,
+                language_aware,
+                highlights,
+            ),
+            input_chunk: Default::default(),
+            output_position: output_start,
+            max_output_row: rows.end,
+            transforms,
+        }
+    }
+
+    pub fn max_point(&self) -> WrapPoint {
+        WrapPoint(self.transforms.summary().output.lines)
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+        cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &());
+        if cursor
+            .item()
+            .map_or(false, |transform| transform.is_isomorphic())
+        {
+            let overshoot = row - cursor.start().0.row();
+            let tab_row = cursor.start().1.row() + overshoot;
+            let tab_line_len = self.tab_snapshot.line_len(tab_row);
+            if overshoot == 0 {
+                cursor.start().0.column() + (tab_line_len - cursor.start().1.column())
+            } else {
+                tab_line_len
+            }
+        } else {
+            cursor.start().0.column()
+        }
+    }
+
+    pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
+        let mut cursor = self.transforms.cursor::<WrapPoint>();
+        cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &());
+        cursor.item().and_then(|transform| {
+            if transform.is_isomorphic() {
+                None
+            } else {
+                Some(transform.summary.output.lines.column)
+            }
+        })
+    }
+
+    pub fn longest_row(&self) -> u32 {
+        self.transforms.summary().output.longest_row
+    }
+
+    pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows {
+        let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+        transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
+        let mut input_row = transforms.start().1.row();
+        if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+            input_row += start_row - transforms.start().0.row();
+        }
+        let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
+        let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row);
+        let input_buffer_row = input_buffer_rows.next().unwrap();
+        WrapBufferRows {
+            transforms,
+            input_buffer_row,
+            input_buffer_rows,
+            output_row: start_row,
+            soft_wrapped,
+            max_output_row: self.max_point().row(),
+        }
+    }
+
+    pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint {
+        let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+        cursor.seek(&point, Bias::Right, &());
+        let mut tab_point = cursor.start().1 .0;
+        if cursor.item().map_or(false, |t| t.is_isomorphic()) {
+            tab_point += point.0 - cursor.start().0 .0;
+        }
+        TabPoint(tab_point)
+    }
+
+    pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point {
+        self.tab_snapshot.to_point(self.to_tab_point(point), bias)
+    }
+
+    pub fn make_wrap_point(&self, point: Point, bias: Bias) -> WrapPoint {
+        self.tab_point_to_wrap_point(self.tab_snapshot.make_tab_point(point, bias))
+    }
+
+    pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint {
+        let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>();
+        cursor.seek(&point, Bias::Right, &());
+        WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0))
+    }
+
+    pub fn clip_point(&self, mut point: WrapPoint, bias: Bias) -> WrapPoint {
+        if bias == Bias::Left {
+            let mut cursor = self.transforms.cursor::<WrapPoint>();
+            cursor.seek(&point, Bias::Right, &());
+            if cursor.item().map_or(false, |t| !t.is_isomorphic()) {
+                point = *cursor.start();
+                *point.column_mut() -= 1;
+            }
+        }
+
+        self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
+    }
+
+    pub fn prev_row_boundary(&self, mut point: WrapPoint) -> u32 {
+        if self.transforms.is_empty() {
+            return 0;
+        }
+
+        *point.column_mut() = 0;
+
+        let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+        cursor.seek(&point, Bias::Right, &());
+        if cursor.item().is_none() {
+            cursor.prev(&());
+        }
+
+        while let Some(transform) = cursor.item() {
+            if transform.is_isomorphic() && cursor.start().1.column() == 0 {
+                return cmp::min(cursor.end(&()).0.row(), point.row());
+            } else {
+                cursor.prev(&());
+            }
+        }
+
+        unreachable!()
+    }
+
+    pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<u32> {
+        point.0 += Point::new(1, 0);
+
+        let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+        cursor.seek(&point, Bias::Right, &());
+        while let Some(transform) = cursor.item() {
+            if transform.is_isomorphic() && cursor.start().1.column() == 0 {
+                return Some(cmp::max(cursor.start().0.row(), point.row()));
+            } else {
+                cursor.next(&());
+            }
+        }
+
+        None
+    }
+
+    fn check_invariants(&self) {
+        #[cfg(test)]
+        {
+            assert_eq!(
+                TabPoint::from(self.transforms.summary().input.lines),
+                self.tab_snapshot.max_point()
+            );
+
+            {
+                let mut transforms = self.transforms.cursor::<()>().peekable();
+                while let Some(transform) = transforms.next() {
+                    if let Some(next_transform) = transforms.peek() {
+                        assert!(transform.is_isomorphic() != next_transform.is_isomorphic());
+                    }
+                }
+            }
+
+            let text = language::Rope::from(self.text().as_str());
+            let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
+            let mut expected_buffer_rows = Vec::new();
+            let mut prev_tab_row = 0;
+            for display_row in 0..=self.max_point().row() {
+                let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
+                if tab_point.row() == prev_tab_row && display_row != 0 {
+                    expected_buffer_rows.push(None);
+                } else {
+                    expected_buffer_rows.push(input_buffer_rows.next().unwrap());
+                }
+
+                prev_tab_row = tab_point.row();
+                assert_eq!(self.line_len(display_row), text.line_len(display_row));
+            }
+
+            for start_display_row in 0..expected_buffer_rows.len() {
+                assert_eq!(
+                    self.buffer_rows(start_display_row as u32)
+                        .collect::<Vec<_>>(),
+                    &expected_buffer_rows[start_display_row..],
+                    "invalid buffer_rows({}..)",
+                    start_display_row
+                );
+            }
+        }
+    }
+}
+
+impl<'a> Iterator for WrapChunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.output_position.row() >= self.max_output_row {
+            return None;
+        }
+
+        let transform = self.transforms.item()?;
+        if let Some(display_text) = transform.display_text {
+            let mut start_ix = 0;
+            let mut end_ix = display_text.len();
+            let mut summary = transform.summary.output.lines;
+
+            if self.output_position > self.transforms.start().0 {
+                // Exclude newline starting prior to the desired row.
+                start_ix = 1;
+                summary.row = 0;
+            } else if self.output_position.row() + 1 >= self.max_output_row {
+                // Exclude soft indentation ending after the desired row.
+                end_ix = 1;
+                summary.column = 0;
+            }
+
+            self.output_position.0 += summary;
+            self.transforms.next(&());
+            return Some(Chunk {
+                text: &display_text[start_ix..end_ix],
+                ..self.input_chunk
+            });
+        }
+
+        if self.input_chunk.text.is_empty() {
+            self.input_chunk = self.input_chunks.next().unwrap();
+        }
+
+        let mut input_len = 0;
+        let transform_end = self.transforms.end(&()).0;
+        for c in self.input_chunk.text.chars() {
+            let char_len = c.len_utf8();
+            input_len += char_len;
+            if c == '\n' {
+                *self.output_position.row_mut() += 1;
+                *self.output_position.column_mut() = 0;
+            } else {
+                *self.output_position.column_mut() += char_len as u32;
+            }
+
+            if self.output_position >= transform_end {
+                self.transforms.next(&());
+                break;
+            }
+        }
+
+        let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
+        self.input_chunk.text = suffix;
+        Some(Chunk {
+            text: prefix,
+            ..self.input_chunk
+        })
+    }
+}
+
+impl<'a> Iterator for WrapBufferRows<'a> {
+    type Item = Option<u32>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.output_row > self.max_output_row {
+            return None;
+        }
+
+        let buffer_row = self.input_buffer_row;
+        let soft_wrapped = self.soft_wrapped;
+
+        self.output_row += 1;
+        self.transforms
+            .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left, &());
+        if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
+            self.input_buffer_row = self.input_buffer_rows.next().unwrap();
+            self.soft_wrapped = false;
+        } else {
+            self.soft_wrapped = true;
+        }
+
+        Some(if soft_wrapped { None } else { buffer_row })
+    }
+}
+
+impl Transform {
+    fn isomorphic(summary: TextSummary) -> Self {
+        #[cfg(test)]
+        assert!(!summary.lines.is_zero());
+
+        Self {
+            summary: TransformSummary {
+                input: summary.clone(),
+                output: summary,
+            },
+            display_text: None,
+        }
+    }
+
+    fn wrap(indent: u32) -> Self {
+        lazy_static! {
+            static ref WRAP_TEXT: String = {
+                let mut wrap_text = String::new();
+                wrap_text.push('\n');
+                wrap_text.extend((0..LineWrapper::MAX_INDENT as usize).map(|_| ' '));
+                wrap_text
+            };
+        }
+
+        Self {
+            summary: TransformSummary {
+                input: TextSummary::default(),
+                output: TextSummary {
+                    lines: Point::new(1, indent),
+                    first_line_chars: 0,
+                    last_line_chars: indent,
+                    longest_row: 1,
+                    longest_row_chars: indent,
+                },
+            },
+            display_text: Some(&WRAP_TEXT[..1 + indent as usize]),
+        }
+    }
+
+    fn is_isomorphic(&self) -> bool {
+        self.display_text.is_none()
+    }
+}
+
+impl sum_tree::Item for Transform {
+    type Summary = TransformSummary;
+
+    fn summary(&self) -> Self::Summary {
+        self.summary.clone()
+    }
+}
+
+fn push_isomorphic(transforms: &mut Vec<Transform>, summary: TextSummary) {
+    if let Some(last_transform) = transforms.last_mut() {
+        if last_transform.is_isomorphic() {
+            last_transform.summary.input += &summary;
+            last_transform.summary.output += &summary;
+            return;
+        }
+    }
+    transforms.push(Transform::isomorphic(summary));
+}
+
+trait SumTreeExt {
+    fn push_or_extend(&mut self, transform: Transform);
+}
+
+impl SumTreeExt for SumTree<Transform> {
+    fn push_or_extend(&mut self, transform: Transform) {
+        let mut transform = Some(transform);
+        self.update_last(
+            |last_transform| {
+                if last_transform.is_isomorphic() && transform.as_ref().unwrap().is_isomorphic() {
+                    let transform = transform.take().unwrap();
+                    last_transform.summary.input += &transform.summary.input;
+                    last_transform.summary.output += &transform.summary.output;
+                }
+            },
+            &(),
+        );
+
+        if let Some(transform) = transform {
+            self.push(transform, &());
+        }
+    }
+}
+
+impl WrapPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(Point::new(row, column))
+    }
+
+    pub fn row(self) -> u32 {
+        self.0.row
+    }
+
+    pub fn row_mut(&mut self) -> &mut u32 {
+        &mut self.0.row
+    }
+
+    pub fn column(self) -> u32 {
+        self.0.column
+    }
+
+    pub fn column_mut(&mut self) -> &mut u32 {
+        &mut self.0.column
+    }
+}
+
+impl sum_tree::Summary for TransformSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, other: &Self, _: &()) {
+        self.input += &other.input;
+        self.output += &other.output;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += summary.input.lines;
+    }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for TabPoint {
+    fn cmp(&self, cursor_location: &TransformSummary, _: &()) -> std::cmp::Ordering {
+        Ord::cmp(&self.0, &cursor_location.input.lines)
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += summary.output.lines;
+    }
+}
+
+fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
+    let mut i = 1;
+    while i < edits.len() {
+        let edit = edits[i].clone();
+        let prev_edit = &mut edits[i - 1];
+        if prev_edit.old.end >= edit.old.start {
+            prev_edit.old.end = edit.old.end;
+            prev_edit.new.end = edit.new.end;
+            edits.remove(i);
+            continue;
+        }
+        i += 1;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
+        MultiBuffer,
+    };
+    use gpui::test::observe;
+    use rand::prelude::*;
+    use settings::SettingsStore;
+    use smol::stream::StreamExt;
+    use std::{cmp, env, num::NonZeroU32};
+    use text::Rope;
+
+    #[gpui::test(iterations = 100)]
+    async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+        init_test(cx);
+
+        cx.foreground().set_block_on_ticks(0..=50);
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let font_cache = cx.font_cache().clone();
+        let font_system = cx.platform().fonts();
+        let mut wrap_width = if rng.gen_bool(0.1) {
+            None
+        } else {
+            Some(rng.gen_range(0.0..=1000.0))
+        };
+        let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+
+        log::info!("Tab size: {}", tab_size);
+        log::info!("Wrap width: {:?}", wrap_width);
+
+        let buffer = cx.update(|cx| {
+            if rng.gen() {
+                MultiBuffer::build_random(&mut rng, cx)
+            } else {
+                let len = rng.gen_range(0..10);
+                let text = util::RandomCharIter::new(&mut rng)
+                    .take(len)
+                    .collect::<String>();
+                MultiBuffer::build_simple(&text, cx)
+            }
+        });
+        let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+        log::info!("Buffer text: {:?}", buffer_snapshot.text());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
+        log::info!("FoldMap text: {:?}", fold_snapshot.text());
+        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
+        let tabs_snapshot = tab_map.set_max_expansion_column(32);
+        log::info!("TabMap text: {:?}", tabs_snapshot.text());
+
+        let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
+        let unwrapped_text = tabs_snapshot.text();
+        let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
+
+        let (wrap_map, _) =
+            cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx));
+        let mut notifications = observe(&wrap_map, cx);
+
+        if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+            notifications.next().await.unwrap();
+        }
+
+        let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| {
+            assert!(!map.is_rewrapping());
+            map.sync(tabs_snapshot.clone(), Vec::new(), cx)
+        });
+
+        let actual_text = initial_snapshot.text();
+        assert_eq!(
+            actual_text, expected_text,
+            "unwrapped text is: {:?}",
+            unwrapped_text
+        );
+        log::info!("Wrapped text: {:?}", actual_text);
+
+        let mut next_inlay_id = 0;
+        let mut edits = Vec::new();
+        for _i in 0..operations {
+            log::info!("{} ==============================================", _i);
+
+            let mut buffer_edits = Vec::new();
+            match rng.gen_range(0..=100) {
+                0..=19 => {
+                    wrap_width = if rng.gen_bool(0.2) {
+                        None
+                    } else {
+                        Some(rng.gen_range(0.0..=1000.0))
+                    };
+                    log::info!("Setting wrap width to {:?}", wrap_width);
+                    wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+                }
+                20..=39 => {
+                    for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
+                        let (tabs_snapshot, tab_edits) =
+                            tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                        let (mut snapshot, wrap_edits) =
+                            wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
+                        snapshot.check_invariants();
+                        snapshot.verify_chunks(&mut rng);
+                        edits.push((snapshot, wrap_edits));
+                    }
+                }
+                40..=59 => {
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+                    let (tabs_snapshot, tab_edits) =
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                    let (mut snapshot, wrap_edits) =
+                        wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
+                    snapshot.check_invariants();
+                    snapshot.verify_chunks(&mut rng);
+                    edits.push((snapshot, wrap_edits));
+                }
+                _ => {
+                    buffer.update(cx, |buffer, cx| {
+                        let subscription = buffer.subscribe();
+                        let edit_count = rng.gen_range(1..=5);
+                        buffer.randomly_mutate(&mut rng, edit_count, cx);
+                        buffer_snapshot = buffer.snapshot(cx);
+                        buffer_edits.extend(subscription.consume());
+                    });
+                }
+            }
+
+            log::info!("Buffer text: {:?}", buffer_snapshot.text());
+            let (inlay_snapshot, inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+            let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+            log::info!("FoldMap text: {:?}", fold_snapshot.text());
+            let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
+            log::info!("TabMap text: {:?}", tabs_snapshot.text());
+
+            let unwrapped_text = tabs_snapshot.text();
+            let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
+            let (mut snapshot, wrap_edits) =
+                wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
+            snapshot.check_invariants();
+            snapshot.verify_chunks(&mut rng);
+            edits.push((snapshot, wrap_edits));
+
+            if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
+                log::info!("Waiting for wrapping to finish");
+                while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+                    notifications.next().await.unwrap();
+                }
+                wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
+            }
+
+            if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+                let (mut wrapped_snapshot, wrap_edits) =
+                    wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
+                let actual_text = wrapped_snapshot.text();
+                let actual_longest_row = wrapped_snapshot.longest_row();
+                log::info!("Wrapping finished: {:?}", actual_text);
+                wrapped_snapshot.check_invariants();
+                wrapped_snapshot.verify_chunks(&mut rng);
+                edits.push((wrapped_snapshot.clone(), wrap_edits));
+                assert_eq!(
+                    actual_text, expected_text,
+                    "unwrapped text is: {:?}",
+                    unwrapped_text
+                );
+
+                let mut summary = TextSummary::default();
+                for (ix, item) in wrapped_snapshot
+                    .transforms
+                    .items(&())
+                    .into_iter()
+                    .enumerate()
+                {
+                    summary += &item.summary.output;
+                    log::info!("{} summary: {:?}", ix, item.summary.output,);
+                }
+
+                if tab_size.get() == 1
+                    || !wrapped_snapshot
+                        .tab_snapshot
+                        .fold_snapshot
+                        .text()
+                        .contains('\t')
+                {
+                    let mut expected_longest_rows = Vec::new();
+                    let mut longest_line_len = -1;
+                    for (row, line) in expected_text.split('\n').enumerate() {
+                        let line_char_count = line.chars().count() as isize;
+                        if line_char_count > longest_line_len {
+                            expected_longest_rows.clear();
+                            longest_line_len = line_char_count;
+                        }
+                        if line_char_count >= longest_line_len {
+                            expected_longest_rows.push(row as u32);
+                        }
+                    }
+
+                    assert!(
+                        expected_longest_rows.contains(&actual_longest_row),
+                        "incorrect longest row {}. expected {:?} with length {}",
+                        actual_longest_row,
+                        expected_longest_rows,
+                        longest_line_len,
+                    )
+                }
+            }
+        }
+
+        let mut initial_text = Rope::from(initial_snapshot.text().as_str());
+        for (snapshot, patch) in edits {
+            let snapshot_text = Rope::from(snapshot.text().as_str());
+            for edit in &patch {
+                let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
+                let old_end = initial_text.point_to_offset(cmp::min(
+                    Point::new(edit.new.start + edit.old.len() as u32, 0),
+                    initial_text.max_point(),
+                ));
+                let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
+                let new_end = snapshot_text.point_to_offset(cmp::min(
+                    Point::new(edit.new.end, 0),
+                    snapshot_text.max_point(),
+                ));
+                let new_text = snapshot_text
+                    .chunks_in_range(new_start..new_end)
+                    .collect::<String>();
+
+                initial_text.replace(old_start..old_end, &new_text);
+            }
+            assert_eq!(initial_text.to_string(), snapshot_text.to_string());
+        }
+
+        if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+            log::info!("Waiting for wrapping to finish");
+            while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+                notifications.next().await.unwrap();
+            }
+        }
+        wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
+    }
+
+    fn init_test(cx: &mut gpui::TestAppContext) {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+        });
+    }
+
+    fn wrap_text(
+        unwrapped_text: &str,
+        wrap_width: Option<f32>,
+        line_wrapper: &mut LineWrapper,
+    ) -> String {
+        if let Some(wrap_width) = wrap_width {
+            let mut wrapped_text = String::new();
+            for (row, line) in unwrapped_text.split('\n').enumerate() {
+                if row > 0 {
+                    wrapped_text.push('\n')
+                }
+
+                let mut prev_ix = 0;
+                for boundary in line_wrapper.wrap_line(line, wrap_width) {
+                    wrapped_text.push_str(&line[prev_ix..boundary.ix]);
+                    wrapped_text.push('\n');
+                    wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
+                    prev_ix = boundary.ix;
+                }
+                wrapped_text.push_str(&line[prev_ix..]);
+            }
+            wrapped_text
+        } else {
+            unwrapped_text.to_string()
+        }
+    }
+
+    impl WrapSnapshot {
+        pub fn text(&self) -> String {
+            self.text_chunks(0).collect()
+        }
+
+        pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
+            self.chunks(
+                wrap_row..self.max_point().row() + 1,
+                false,
+                Highlights::default(),
+            )
+            .map(|h| h.text)
+        }
+
+        fn verify_chunks(&mut self, rng: &mut impl Rng) {
+            for _ in 0..5 {
+                let mut end_row = rng.gen_range(0..=self.max_point().row());
+                let start_row = rng.gen_range(0..=end_row);
+                end_row += 1;
+
+                let mut expected_text = self.text_chunks(start_row).collect::<String>();
+                if expected_text.ends_with('\n') {
+                    expected_text.push('\n');
+                }
+                let mut expected_text = expected_text
+                    .lines()
+                    .take((end_row - start_row) as usize)
+                    .collect::<Vec<_>>()
+                    .join("\n");
+                if end_row <= self.max_point().row() {
+                    expected_text.push('\n');
+                }
+
+                let actual_text = self
+                    .chunks(start_row..end_row, true, Highlights::default())
+                    .map(|c| c.text)
+                    .collect::<String>();
+                assert_eq!(
+                    expected_text,
+                    actual_text,
+                    "chunks != highlighted_chunks for rows {:?}",
+                    start_row..end_row
+                );
+            }
+        }
+    }
+}

crates/editor2/src/editor.rs ๐Ÿ”—

@@ -0,0 +1,10095 @@
+mod blink_manager;
+pub mod display_map;
+mod editor_settings;
+mod element;
+mod inlay_hint_cache;
+
+mod git;
+mod highlight_matching_bracket;
+mod hover_popover;
+pub mod items;
+mod link_go_to_definition;
+mod mouse_context_menu;
+pub mod movement;
+mod persistence;
+pub mod scroll;
+pub mod selections_collection;
+
+#[cfg(test)]
+mod editor_tests;
+#[cfg(any(test, feature = "test-support"))]
+pub mod test;
+
+use ::git::diff::DiffHunk;
+use aho_corasick::AhoCorasick;
+use anyhow::{anyhow, Context, Result};
+use blink_manager::BlinkManager;
+use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings};
+use clock::{Global, ReplicaId};
+use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
+use convert_case::{Case, Casing};
+use copilot::Copilot;
+pub use display_map::DisplayPoint;
+use display_map::*;
+pub use editor_settings::EditorSettings;
+pub use element::{
+    Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
+};
+use futures::FutureExt;
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+    actions,
+    color::Color,
+    elements::*,
+    executor,
+    fonts::{self, HighlightStyle, TextStyle},
+    geometry::vector::{vec2f, Vector2F},
+    impl_actions,
+    keymap_matcher::KeymapContext,
+    platform::{CursorStyle, MouseButton},
+    serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem,
+    CursorRegion, Element, Entity, ModelHandle, MouseRegion, Subscription, Task, View, ViewContext,
+    ViewHandle, WeakViewHandle, WindowContext,
+};
+use highlight_matching_bracket::refresh_matching_bracket_highlights;
+use hover_popover::{hide_hover, HoverState};
+use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
+pub use items::MAX_TAB_TITLE_LEN;
+use itertools::Itertools;
+pub use language::{char_kind, CharKind};
+use language::{
+    language_settings::{self, all_language_settings, InlayHintSettings},
+    markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel,
+    Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind,
+    IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point,
+    Selection, SelectionGoal, TransactionId,
+};
+use link_go_to_definition::{
+    hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight,
+    LinkGoToDefinitionState,
+};
+use log::error;
+use lsp::LanguageServerId;
+use movement::TextLayoutDetails;
+use multi_buffer::ToOffsetUtf16;
+pub use multi_buffer::{
+    Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
+    ToPoint,
+};
+use ordered_float::OrderedFloat;
+use parking_lot::RwLock;
+use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
+use rand::{seq::SliceRandom, thread_rng};
+use rpc::proto::{self, PeerId};
+use scroll::{
+    autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
+};
+use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
+use serde::{Deserialize, Serialize};
+use settings::SettingsStore;
+use smallvec::SmallVec;
+use snippet::Snippet;
+use std::{
+    any::TypeId,
+    borrow::Cow,
+    cmp::{self, Ordering, Reverse},
+    mem,
+    num::NonZeroU32,
+    ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
+    path::Path,
+    sync::Arc,
+    time::{Duration, Instant},
+};
+pub use sum_tree::Bias;
+use sum_tree::TreeMap;
+use text::Rope;
+use theme::{DiagnosticStyle, Theme, ThemeSettings};
+use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
+use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace};
+
+use crate::git::diff_hunk_to_display;
+
+const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
+const MAX_LINE_LEN: usize = 1024;
+const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
+const MAX_SELECTION_HISTORY_LEN: usize = 1024;
+const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
+pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
+pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
+
+pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
+
+pub fn render_parsed_markdown<Tag: 'static>(
+    parsed: &language::ParsedMarkdown,
+    editor_style: &EditorStyle,
+    workspace: Option<WeakViewHandle<Workspace>>,
+    cx: &mut ViewContext<Editor>,
+) -> Text {
+    enum RenderedMarkdown {}
+
+    let parsed = parsed.clone();
+    let view_id = cx.view_id();
+    let code_span_background_color = editor_style.document_highlight_read_background;
+
+    let mut region_id = 0;
+
+    Text::new(parsed.text, editor_style.text.clone())
+        .with_highlights(
+            parsed
+                .highlights
+                .iter()
+                .filter_map(|(range, highlight)| {
+                    let highlight = highlight.to_highlight_style(&editor_style.syntax)?;
+                    Some((range.clone(), highlight))
+                })
+                .collect::<Vec<_>>(),
+        )
+        .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| {
+            region_id += 1;
+            let region = parsed.regions[ix].clone();
+
+            if let Some(link) = region.link {
+                cx.scene().push_cursor_region(CursorRegion {
+                    bounds,
+                    style: CursorStyle::PointingHand,
+                });
+                cx.scene().push_mouse_region(
+                    MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds)
+                        .on_down::<Editor, _>(MouseButton::Left, move |_, _, cx| match &link {
+                            markdown::Link::Web { url } => cx.platform().open_url(url),
+                            markdown::Link::Path { path } => {
+                                if let Some(workspace) = &workspace {
+                                    _ = workspace.update(cx, |workspace, cx| {
+                                        workspace.open_abs_path(path.clone(), false, cx).detach();
+                                    });
+                                }
+                            }
+                        }),
+                );
+            }
+
+            if region.code {
+                cx.scene().push_quad(gpui::Quad {
+                    bounds,
+                    background: Some(code_span_background_color),
+                    border: Default::default(),
+                    corner_radii: (2.0).into(),
+                });
+            }
+        })
+        .with_soft_wrap(true)
+}
+
+#[derive(Clone, Deserialize, PartialEq, Default)]
+pub struct SelectNext {
+    #[serde(default)]
+    pub replace_newest: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq, Default)]
+pub struct SelectPrevious {
+    #[serde(default)]
+    pub replace_newest: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq, Default)]
+pub struct SelectAllMatches {
+    #[serde(default)]
+    pub replace_newest: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct SelectToBeginningOfLine {
+    #[serde(default)]
+    stop_at_soft_wraps: bool,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct MovePageUp {
+    #[serde(default)]
+    center_cursor: bool,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct MovePageDown {
+    #[serde(default)]
+    center_cursor: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct SelectToEndOfLine {
+    #[serde(default)]
+    stop_at_soft_wraps: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct ToggleCodeActions {
+    #[serde(default)]
+    pub deployed_from_indicator: bool,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct ConfirmCompletion {
+    #[serde(default)]
+    pub item_ix: Option<usize>,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct ConfirmCodeAction {
+    #[serde(default)]
+    pub item_ix: Option<usize>,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct ToggleComments {
+    #[serde(default)]
+    pub advance_downwards: bool,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct FoldAt {
+    pub buffer_row: u32,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct UnfoldAt {
+    pub buffer_row: u32,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct GutterHover {
+    pub hovered: bool,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum InlayId {
+    Suggestion(usize),
+    Hint(usize),
+}
+
+impl InlayId {
+    fn id(&self) -> usize {
+        match self {
+            Self::Suggestion(id) => *id,
+            Self::Hint(id) => *id,
+        }
+    }
+}
+
+actions!(
+    editor,
+    [
+        Cancel,
+        Backspace,
+        Delete,
+        Newline,
+        NewlineAbove,
+        NewlineBelow,
+        GoToDiagnostic,
+        GoToPrevDiagnostic,
+        GoToHunk,
+        GoToPrevHunk,
+        Indent,
+        Outdent,
+        DeleteLine,
+        DeleteToPreviousWordStart,
+        DeleteToPreviousSubwordStart,
+        DeleteToNextWordEnd,
+        DeleteToNextSubwordEnd,
+        DeleteToBeginningOfLine,
+        DeleteToEndOfLine,
+        CutToEndOfLine,
+        DuplicateLine,
+        MoveLineUp,
+        MoveLineDown,
+        JoinLines,
+        SortLinesCaseSensitive,
+        SortLinesCaseInsensitive,
+        ReverseLines,
+        ShuffleLines,
+        ConvertToUpperCase,
+        ConvertToLowerCase,
+        ConvertToTitleCase,
+        ConvertToSnakeCase,
+        ConvertToKebabCase,
+        ConvertToUpperCamelCase,
+        ConvertToLowerCamelCase,
+        Transpose,
+        Cut,
+        Copy,
+        Paste,
+        Undo,
+        Redo,
+        MoveUp,
+        PageUp,
+        MoveDown,
+        PageDown,
+        MoveLeft,
+        MoveRight,
+        MoveToPreviousWordStart,
+        MoveToPreviousSubwordStart,
+        MoveToNextWordEnd,
+        MoveToNextSubwordEnd,
+        MoveToBeginningOfLine,
+        MoveToEndOfLine,
+        MoveToStartOfParagraph,
+        MoveToEndOfParagraph,
+        MoveToBeginning,
+        MoveToEnd,
+        SelectUp,
+        SelectDown,
+        SelectLeft,
+        SelectRight,
+        SelectToPreviousWordStart,
+        SelectToPreviousSubwordStart,
+        SelectToNextWordEnd,
+        SelectToNextSubwordEnd,
+        SelectToStartOfParagraph,
+        SelectToEndOfParagraph,
+        SelectToBeginning,
+        SelectToEnd,
+        SelectAll,
+        SelectLine,
+        SplitSelectionIntoLines,
+        AddSelectionAbove,
+        AddSelectionBelow,
+        Tab,
+        TabPrev,
+        ShowCharacterPalette,
+        SelectLargerSyntaxNode,
+        SelectSmallerSyntaxNode,
+        GoToDefinition,
+        GoToDefinitionSplit,
+        GoToTypeDefinition,
+        GoToTypeDefinitionSplit,
+        MoveToEnclosingBracket,
+        UndoSelection,
+        RedoSelection,
+        FindAllReferences,
+        Rename,
+        ConfirmRename,
+        Fold,
+        UnfoldLines,
+        FoldSelectedRanges,
+        ShowCompletions,
+        OpenExcerpts,
+        RestartLanguageServer,
+        Hover,
+        Format,
+        ToggleSoftWrap,
+        ToggleInlayHints,
+        RevealInFinder,
+        CopyPath,
+        CopyRelativePath,
+        CopyHighlightJson,
+        ContextMenuFirst,
+        ContextMenuPrev,
+        ContextMenuNext,
+        ContextMenuLast,
+    ]
+);
+
+impl_actions!(
+    editor,
+    [
+        SelectNext,
+        SelectPrevious,
+        SelectAllMatches,
+        SelectToBeginningOfLine,
+        SelectToEndOfLine,
+        ToggleCodeActions,
+        MovePageUp,
+        MovePageDown,
+        ConfirmCompletion,
+        ConfirmCodeAction,
+        ToggleComments,
+        FoldAt,
+        UnfoldAt,
+        GutterHover
+    ]
+);
+
+enum DocumentHighlightRead {}
+enum DocumentHighlightWrite {}
+enum InputComposition {}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum Direction {
+    Prev,
+    Next,
+}
+
+pub fn init_settings(cx: &mut AppContext) {
+    settings::register::<EditorSettings>(cx);
+}
+
+pub fn init(cx: &mut AppContext) {
+    init_settings(cx);
+    cx.add_action(Editor::new_file);
+    cx.add_action(Editor::new_file_in_direction);
+    cx.add_action(Editor::cancel);
+    cx.add_action(Editor::newline);
+    cx.add_action(Editor::newline_above);
+    cx.add_action(Editor::newline_below);
+    cx.add_action(Editor::backspace);
+    cx.add_action(Editor::delete);
+    cx.add_action(Editor::tab);
+    cx.add_action(Editor::tab_prev);
+    cx.add_action(Editor::indent);
+    cx.add_action(Editor::outdent);
+    cx.add_action(Editor::delete_line);
+    cx.add_action(Editor::join_lines);
+    cx.add_action(Editor::sort_lines_case_sensitive);
+    cx.add_action(Editor::sort_lines_case_insensitive);
+    cx.add_action(Editor::reverse_lines);
+    cx.add_action(Editor::shuffle_lines);
+    cx.add_action(Editor::convert_to_upper_case);
+    cx.add_action(Editor::convert_to_lower_case);
+    cx.add_action(Editor::convert_to_title_case);
+    cx.add_action(Editor::convert_to_snake_case);
+    cx.add_action(Editor::convert_to_kebab_case);
+    cx.add_action(Editor::convert_to_upper_camel_case);
+    cx.add_action(Editor::convert_to_lower_camel_case);
+    cx.add_action(Editor::delete_to_previous_word_start);
+    cx.add_action(Editor::delete_to_previous_subword_start);
+    cx.add_action(Editor::delete_to_next_word_end);
+    cx.add_action(Editor::delete_to_next_subword_end);
+    cx.add_action(Editor::delete_to_beginning_of_line);
+    cx.add_action(Editor::delete_to_end_of_line);
+    cx.add_action(Editor::cut_to_end_of_line);
+    cx.add_action(Editor::duplicate_line);
+    cx.add_action(Editor::move_line_up);
+    cx.add_action(Editor::move_line_down);
+    cx.add_action(Editor::transpose);
+    cx.add_action(Editor::cut);
+    cx.add_action(Editor::copy);
+    cx.add_action(Editor::paste);
+    cx.add_action(Editor::undo);
+    cx.add_action(Editor::redo);
+    cx.add_action(Editor::move_up);
+    cx.add_action(Editor::move_page_up);
+    cx.add_action(Editor::move_down);
+    cx.add_action(Editor::move_page_down);
+    cx.add_action(Editor::next_screen);
+    cx.add_action(Editor::move_left);
+    cx.add_action(Editor::move_right);
+    cx.add_action(Editor::move_to_previous_word_start);
+    cx.add_action(Editor::move_to_previous_subword_start);
+    cx.add_action(Editor::move_to_next_word_end);
+    cx.add_action(Editor::move_to_next_subword_end);
+    cx.add_action(Editor::move_to_beginning_of_line);
+    cx.add_action(Editor::move_to_end_of_line);
+    cx.add_action(Editor::move_to_start_of_paragraph);
+    cx.add_action(Editor::move_to_end_of_paragraph);
+    cx.add_action(Editor::move_to_beginning);
+    cx.add_action(Editor::move_to_end);
+    cx.add_action(Editor::select_up);
+    cx.add_action(Editor::select_down);
+    cx.add_action(Editor::select_left);
+    cx.add_action(Editor::select_right);
+    cx.add_action(Editor::select_to_previous_word_start);
+    cx.add_action(Editor::select_to_previous_subword_start);
+    cx.add_action(Editor::select_to_next_word_end);
+    cx.add_action(Editor::select_to_next_subword_end);
+    cx.add_action(Editor::select_to_beginning_of_line);
+    cx.add_action(Editor::select_to_end_of_line);
+    cx.add_action(Editor::select_to_start_of_paragraph);
+    cx.add_action(Editor::select_to_end_of_paragraph);
+    cx.add_action(Editor::select_to_beginning);
+    cx.add_action(Editor::select_to_end);
+    cx.add_action(Editor::select_all);
+    cx.add_action(Editor::select_all_matches);
+    cx.add_action(Editor::select_line);
+    cx.add_action(Editor::split_selection_into_lines);
+    cx.add_action(Editor::add_selection_above);
+    cx.add_action(Editor::add_selection_below);
+    cx.add_action(Editor::select_next);
+    cx.add_action(Editor::select_previous);
+    cx.add_action(Editor::toggle_comments);
+    cx.add_action(Editor::select_larger_syntax_node);
+    cx.add_action(Editor::select_smaller_syntax_node);
+    cx.add_action(Editor::move_to_enclosing_bracket);
+    cx.add_action(Editor::undo_selection);
+    cx.add_action(Editor::redo_selection);
+    cx.add_action(Editor::go_to_diagnostic);
+    cx.add_action(Editor::go_to_prev_diagnostic);
+    cx.add_action(Editor::go_to_hunk);
+    cx.add_action(Editor::go_to_prev_hunk);
+    cx.add_action(Editor::go_to_definition);
+    cx.add_action(Editor::go_to_definition_split);
+    cx.add_action(Editor::go_to_type_definition);
+    cx.add_action(Editor::go_to_type_definition_split);
+    cx.add_action(Editor::fold);
+    cx.add_action(Editor::fold_at);
+    cx.add_action(Editor::unfold_lines);
+    cx.add_action(Editor::unfold_at);
+    cx.add_action(Editor::gutter_hover);
+    cx.add_action(Editor::fold_selected_ranges);
+    cx.add_action(Editor::show_completions);
+    cx.add_action(Editor::toggle_code_actions);
+    cx.add_action(Editor::open_excerpts);
+    cx.add_action(Editor::toggle_soft_wrap);
+    cx.add_action(Editor::toggle_inlay_hints);
+    cx.add_action(Editor::reveal_in_finder);
+    cx.add_action(Editor::copy_path);
+    cx.add_action(Editor::copy_relative_path);
+    cx.add_action(Editor::copy_highlight_json);
+    cx.add_async_action(Editor::format);
+    cx.add_action(Editor::restart_language_server);
+    cx.add_action(Editor::show_character_palette);
+    cx.add_async_action(Editor::confirm_completion);
+    cx.add_async_action(Editor::confirm_code_action);
+    cx.add_async_action(Editor::rename);
+    cx.add_async_action(Editor::confirm_rename);
+    cx.add_async_action(Editor::find_all_references);
+    cx.add_action(Editor::next_copilot_suggestion);
+    cx.add_action(Editor::previous_copilot_suggestion);
+    cx.add_action(Editor::copilot_suggest);
+    cx.add_action(Editor::context_menu_first);
+    cx.add_action(Editor::context_menu_prev);
+    cx.add_action(Editor::context_menu_next);
+    cx.add_action(Editor::context_menu_last);
+
+    hover_popover::init(cx);
+    scroll::actions::init(cx);
+
+    workspace::register_project_item::<Editor>(cx);
+    workspace::register_followable_item::<Editor>(cx);
+    workspace::register_deserializable_item::<Editor>(cx);
+}
+
+trait InvalidationRegion {
+    fn ranges(&self) -> &[Range<Anchor>];
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum SelectPhase {
+    Begin {
+        position: DisplayPoint,
+        add: bool,
+        click_count: usize,
+    },
+    BeginColumnar {
+        position: DisplayPoint,
+        goal_column: u32,
+    },
+    Extend {
+        position: DisplayPoint,
+        click_count: usize,
+    },
+    Update {
+        position: DisplayPoint,
+        goal_column: u32,
+        scroll_position: Vector2F,
+    },
+    End,
+}
+
+#[derive(Clone, Debug)]
+pub enum SelectMode {
+    Character,
+    Word(Range<Anchor>),
+    Line(Range<Anchor>),
+    All,
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum EditorMode {
+    SingleLine,
+    AutoHeight { max_lines: usize },
+    Full,
+}
+
+#[derive(Clone, Debug)]
+pub enum SoftWrap {
+    None,
+    EditorWidth,
+    Column(u32),
+}
+
+#[derive(Clone)]
+pub struct EditorStyle {
+    pub text: TextStyle,
+    pub line_height_scalar: f32,
+    pub placeholder_text: Option<TextStyle>,
+    pub theme: theme::Editor,
+    pub theme_id: usize,
+}
+
+type CompletionId = usize;
+
+type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
+type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
+
+type BackgroundHighlight = (fn(&Theme) -> Color, Vec<Range<Anchor>>);
+type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec<InlayHighlight>);
+
+pub struct Editor {
+    handle: WeakViewHandle<Self>,
+    buffer: ModelHandle<MultiBuffer>,
+    display_map: ModelHandle<DisplayMap>,
+    pub selections: SelectionsCollection,
+    pub scroll_manager: ScrollManager,
+    columnar_selection_tail: Option<Anchor>,
+    add_selections_state: Option<AddSelectionsState>,
+    select_next_state: Option<SelectNextState>,
+    select_prev_state: Option<SelectNextState>,
+    selection_history: SelectionHistory,
+    autoclose_regions: Vec<AutocloseRegion>,
+    snippet_stack: InvalidationStack<SnippetState>,
+    select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
+    ime_transaction: Option<TransactionId>,
+    active_diagnostics: Option<ActiveDiagnosticGroup>,
+    soft_wrap_mode_override: Option<language_settings::SoftWrap>,
+    get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
+    override_text_style: Option<Box<OverrideTextStyle>>,
+    project: Option<ModelHandle<Project>>,
+    collaboration_hub: Option<Box<dyn CollaborationHub>>,
+    focused: bool,
+    blink_manager: ModelHandle<BlinkManager>,
+    pub show_local_selections: bool,
+    mode: EditorMode,
+    show_gutter: bool,
+    show_wrap_guides: Option<bool>,
+    placeholder_text: Option<Arc<str>>,
+    highlighted_rows: Option<Range<u32>>,
+    background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
+    inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
+    nav_history: Option<ItemNavHistory>,
+    context_menu: RwLock<Option<ContextMenu>>,
+    mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
+    completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
+    next_completion_id: CompletionId,
+    available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
+    code_actions_task: Option<Task<()>>,
+    document_highlights_task: Option<Task<()>>,
+    pending_rename: Option<RenameState>,
+    searchable: bool,
+    cursor_shape: CursorShape,
+    collapse_matches: bool,
+    autoindent_mode: Option<AutoindentMode>,
+    workspace: Option<(WeakViewHandle<Workspace>, i64)>,
+    keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
+    input_enabled: bool,
+    read_only: bool,
+    leader_peer_id: Option<PeerId>,
+    remote_id: Option<ViewId>,
+    hover_state: HoverState,
+    gutter_hovered: bool,
+    link_go_to_definition_state: LinkGoToDefinitionState,
+    copilot_state: CopilotState,
+    inlay_hint_cache: InlayHintCache,
+    next_inlay_id: usize,
+    _subscriptions: Vec<Subscription>,
+    pixel_position_of_newest_cursor: Option<Vector2F>,
+}
+
+pub struct EditorSnapshot {
+    pub mode: EditorMode,
+    pub show_gutter: bool,
+    pub display_snapshot: DisplaySnapshot,
+    pub placeholder_text: Option<Arc<str>>,
+    is_focused: bool,
+    scroll_anchor: ScrollAnchor,
+    ongoing_scroll: OngoingScroll,
+}
+
+pub struct RemoteSelection {
+    pub replica_id: ReplicaId,
+    pub selection: Selection<Anchor>,
+    pub cursor_shape: CursorShape,
+    pub peer_id: PeerId,
+    pub line_mode: bool,
+    pub participant_index: Option<ParticipantIndex>,
+}
+
+#[derive(Clone, Debug)]
+struct SelectionHistoryEntry {
+    selections: Arc<[Selection<Anchor>]>,
+    select_next_state: Option<SelectNextState>,
+    select_prev_state: Option<SelectNextState>,
+    add_selections_state: Option<AddSelectionsState>,
+}
+
+enum SelectionHistoryMode {
+    Normal,
+    Undoing,
+    Redoing,
+}
+
+impl Default for SelectionHistoryMode {
+    fn default() -> Self {
+        Self::Normal
+    }
+}
+
+#[derive(Default)]
+struct SelectionHistory {
+    #[allow(clippy::type_complexity)]
+    selections_by_transaction:
+        HashMap<TransactionId, (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)>,
+    mode: SelectionHistoryMode,
+    undo_stack: VecDeque<SelectionHistoryEntry>,
+    redo_stack: VecDeque<SelectionHistoryEntry>,
+}
+
+impl SelectionHistory {
+    fn insert_transaction(
+        &mut self,
+        transaction_id: TransactionId,
+        selections: Arc<[Selection<Anchor>]>,
+    ) {
+        self.selections_by_transaction
+            .insert(transaction_id, (selections, None));
+    }
+
+    #[allow(clippy::type_complexity)]
+    fn transaction(
+        &self,
+        transaction_id: TransactionId,
+    ) -> Option<&(Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)> {
+        self.selections_by_transaction.get(&transaction_id)
+    }
+
+    #[allow(clippy::type_complexity)]
+    fn transaction_mut(
+        &mut self,
+        transaction_id: TransactionId,
+    ) -> Option<&mut (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)> {
+        self.selections_by_transaction.get_mut(&transaction_id)
+    }
+
+    fn push(&mut self, entry: SelectionHistoryEntry) {
+        if !entry.selections.is_empty() {
+            match self.mode {
+                SelectionHistoryMode::Normal => {
+                    self.push_undo(entry);
+                    self.redo_stack.clear();
+                }
+                SelectionHistoryMode::Undoing => self.push_redo(entry),
+                SelectionHistoryMode::Redoing => self.push_undo(entry),
+            }
+        }
+    }
+
+    fn push_undo(&mut self, entry: SelectionHistoryEntry) {
+        if self
+            .undo_stack
+            .back()
+            .map_or(true, |e| e.selections != entry.selections)
+        {
+            self.undo_stack.push_back(entry);
+            if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN {
+                self.undo_stack.pop_front();
+            }
+        }
+    }
+
+    fn push_redo(&mut self, entry: SelectionHistoryEntry) {
+        if self
+            .redo_stack
+            .back()
+            .map_or(true, |e| e.selections != entry.selections)
+        {
+            self.redo_stack.push_back(entry);
+            if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN {
+                self.redo_stack.pop_front();
+            }
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+struct AddSelectionsState {
+    above: bool,
+    stack: Vec<usize>,
+}
+
+#[derive(Clone)]
+struct SelectNextState {
+    query: AhoCorasick,
+    wordwise: bool,
+    done: bool,
+}
+
+impl std::fmt::Debug for SelectNextState {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct(std::any::type_name::<Self>())
+            .field("wordwise", &self.wordwise)
+            .field("done", &self.done)
+            .finish()
+    }
+}
+
+#[derive(Debug)]
+struct AutocloseRegion {
+    selection_id: usize,
+    range: Range<Anchor>,
+    pair: BracketPair,
+}
+
+#[derive(Debug)]
+struct SnippetState {
+    ranges: Vec<Vec<Range<Anchor>>>,
+    active_index: usize,
+}
+
+pub struct RenameState {
+    pub range: Range<Anchor>,
+    pub old_name: Arc<str>,
+    pub editor: ViewHandle<Editor>,
+    block_id: BlockId,
+}
+
+struct InvalidationStack<T>(Vec<T>);
+
+enum ContextMenu {
+    Completions(CompletionsMenu),
+    CodeActions(CodeActionsMenu),
+}
+
+impl ContextMenu {
+    fn select_first(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        if self.visible() {
+            match self {
+                ContextMenu::Completions(menu) => menu.select_first(project, cx),
+                ContextMenu::CodeActions(menu) => menu.select_first(cx),
+            }
+            true
+        } else {
+            false
+        }
+    }
+
+    fn select_prev(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        if self.visible() {
+            match self {
+                ContextMenu::Completions(menu) => menu.select_prev(project, cx),
+                ContextMenu::CodeActions(menu) => menu.select_prev(cx),
+            }
+            true
+        } else {
+            false
+        }
+    }
+
+    fn select_next(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        if self.visible() {
+            match self {
+                ContextMenu::Completions(menu) => menu.select_next(project, cx),
+                ContextMenu::CodeActions(menu) => menu.select_next(cx),
+            }
+            true
+        } else {
+            false
+        }
+    }
+
+    fn select_last(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        if self.visible() {
+            match self {
+                ContextMenu::Completions(menu) => menu.select_last(project, cx),
+                ContextMenu::CodeActions(menu) => menu.select_last(cx),
+            }
+            true
+        } else {
+            false
+        }
+    }
+
+    fn visible(&self) -> bool {
+        match self {
+            ContextMenu::Completions(menu) => menu.visible(),
+            ContextMenu::CodeActions(menu) => menu.visible(),
+        }
+    }
+
+    fn render(
+        &self,
+        cursor_position: DisplayPoint,
+        style: EditorStyle,
+        workspace: Option<WeakViewHandle<Workspace>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> (DisplayPoint, AnyElement<Editor>) {
+        match self {
+            ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)),
+            ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
+        }
+    }
+}
+
+#[derive(Clone)]
+struct CompletionsMenu {
+    id: CompletionId,
+    initial_position: Anchor,
+    buffer: ModelHandle<Buffer>,
+    completions: Arc<RwLock<Box<[Completion]>>>,
+    match_candidates: Arc<[StringMatchCandidate]>,
+    matches: Arc<[StringMatch]>,
+    selected_item: usize,
+    list: UniformListState,
+}
+
+impl CompletionsMenu {
+    fn select_first(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        self.selected_item = 0;
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.attempt_resolve_selected_completion_documentation(project, cx);
+        cx.notify();
+    }
+
+    fn select_prev(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        if self.selected_item > 0 {
+            self.selected_item -= 1;
+        } else {
+            self.selected_item = self.matches.len() - 1;
+        }
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.attempt_resolve_selected_completion_documentation(project, cx);
+        cx.notify();
+    }
+
+    fn select_next(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        if self.selected_item + 1 < self.matches.len() {
+            self.selected_item += 1;
+        } else {
+            self.selected_item = 0;
+        }
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.attempt_resolve_selected_completion_documentation(project, cx);
+        cx.notify();
+    }
+
+    fn select_last(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        self.selected_item = self.matches.len() - 1;
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.attempt_resolve_selected_completion_documentation(project, cx);
+        cx.notify();
+    }
+
+    fn pre_resolve_completion_documentation(
+        &self,
+        project: Option<ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let settings = settings::get::<EditorSettings>(cx);
+        if !settings.show_completion_documentation {
+            return;
+        }
+
+        let Some(project) = project else {
+            return;
+        };
+        let client = project.read(cx).client();
+        let language_registry = project.read(cx).languages().clone();
+
+        let is_remote = project.read(cx).is_remote();
+        let project_id = project.read(cx).remote_id();
+
+        let completions = self.completions.clone();
+        let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
+
+        cx.spawn(move |this, mut cx| async move {
+            if is_remote {
+                let Some(project_id) = project_id else {
+                    log::error!("Remote project without remote_id");
+                    return;
+                };
+
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
+
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    Self::resolve_completion_documentation_remote(
+                        project_id,
+                        server_id,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        client.clone(),
+                        language_registry.clone(),
+                    )
+                    .await;
+
+                    _ = this.update(&mut cx, |_, cx| cx.notify());
+                }
+            } else {
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
+
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    let server = project.read_with(&mut cx, |project, _| {
+                        project.language_server_for_id(server_id)
+                    });
+                    let Some(server) = server else {
+                        return;
+                    };
+
+                    Self::resolve_completion_documentation_local(
+                        server,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        language_registry.clone(),
+                    )
+                    .await;
+
+                    _ = this.update(&mut cx, |_, cx| cx.notify());
+                }
+            }
+        })
+        .detach();
+    }
+
+    fn attempt_resolve_selected_completion_documentation(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let settings = settings::get::<EditorSettings>(cx);
+        if !settings.show_completion_documentation {
+            return;
+        }
+
+        let completion_index = self.matches[self.selected_item].candidate_id;
+        let Some(project) = project else {
+            return;
+        };
+        let language_registry = project.read(cx).languages().clone();
+
+        let completions = self.completions.clone();
+        let completions_guard = completions.read();
+        let completion = &completions_guard[completion_index];
+        if completion.documentation.is_some() {
+            return;
+        }
+
+        let server_id = completion.server_id;
+        let completion = completion.lsp_completion.clone();
+        drop(completions_guard);
+
+        if project.read(cx).is_remote() {
+            let Some(project_id) = project.read(cx).remote_id() else {
+                log::error!("Remote project without remote_id");
+                return;
+            };
+
+            let client = project.read(cx).client();
+
+            cx.spawn(move |this, mut cx| async move {
+                Self::resolve_completion_documentation_remote(
+                    project_id,
+                    server_id,
+                    completions.clone(),
+                    completion_index,
+                    completion,
+                    client,
+                    language_registry.clone(),
+                )
+                .await;
+
+                _ = this.update(&mut cx, |_, cx| cx.notify());
+            })
+            .detach();
+        } else {
+            let Some(server) = project.read(cx).language_server_for_id(server_id) else {
+                return;
+            };
+
+            cx.spawn(move |this, mut cx| async move {
+                Self::resolve_completion_documentation_local(
+                    server,
+                    completions,
+                    completion_index,
+                    completion,
+                    language_registry,
+                )
+                .await;
+
+                _ = this.update(&mut cx, |_, cx| cx.notify());
+            })
+            .detach();
+        }
+    }
+
+    async fn resolve_completion_documentation_remote(
+        project_id: u64,
+        server_id: LanguageServerId,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        completion_index: usize,
+        completion: lsp::CompletionItem,
+        client: Arc<Client>,
+        language_registry: Arc<LanguageRegistry>,
+    ) {
+        let request = proto::ResolveCompletionDocumentation {
+            project_id,
+            language_server_id: server_id.0 as u64,
+            lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
+        };
+
+        let Some(response) = client
+            .request(request)
+            .await
+            .context("completion documentation resolve proto request")
+            .log_err()
+        else {
+            return;
+        };
+
+        if response.text.is_empty() {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
+
+        let documentation = if response.is_markdown {
+            Documentation::MultiLineMarkdown(
+                markdown::parse_markdown(&response.text, &language_registry, None).await,
+            )
+        } else if response.text.lines().count() <= 1 {
+            Documentation::SingleLine(response.text)
+        } else {
+            Documentation::MultiLinePlainText(response.text)
+        };
+
+        let mut completions = completions.write();
+        let completion = &mut completions[completion_index];
+        completion.documentation = Some(documentation);
+    }
+
+    async fn resolve_completion_documentation_local(
+        server: Arc<lsp::LanguageServer>,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        completion_index: usize,
+        completion: lsp::CompletionItem,
+        language_registry: Arc<LanguageRegistry>,
+    ) {
+        let can_resolve = server
+            .capabilities()
+            .completion_provider
+            .as_ref()
+            .and_then(|options| options.resolve_provider)
+            .unwrap_or(false);
+        if !can_resolve {
+            return;
+        }
+
+        let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
+        let Some(completion_item) = request.await.log_err() else {
+            return;
+        };
+
+        if let Some(lsp_documentation) = completion_item.documentation {
+            let documentation = language::prepare_completion_documentation(
+                &lsp_documentation,
+                &language_registry,
+                None, // TODO: Try to reasonably work out which language the completion is for
+            )
+            .await;
+
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(documentation);
+        } else {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
+    }
+
+    fn visible(&self) -> bool {
+        !self.matches.is_empty()
+    }
+
+    fn render(
+        &self,
+        style: EditorStyle,
+        workspace: Option<WeakViewHandle<Workspace>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> AnyElement<Editor> {
+        enum CompletionTag {}
+
+        let settings = settings::get::<EditorSettings>(cx);
+        let show_completion_documentation = settings.show_completion_documentation;
+
+        let widest_completion_ix = self
+            .matches
+            .iter()
+            .enumerate()
+            .max_by_key(|(_, mat)| {
+                let completions = self.completions.read();
+                let completion = &completions[mat.candidate_id];
+                let documentation = &completion.documentation;
+
+                let mut len = completion.label.text.chars().count();
+                if let Some(Documentation::SingleLine(text)) = documentation {
+                    if show_completion_documentation {
+                        len += text.chars().count();
+                    }
+                }
+
+                len
+            })
+            .map(|(ix, _)| ix);
+
+        let completions = self.completions.clone();
+        let matches = self.matches.clone();
+        let selected_item = self.selected_item;
+
+        let list = UniformList::new(self.list.clone(), matches.len(), cx, {
+            let style = style.clone();
+            move |_, range, items, cx| {
+                let start_ix = range.start;
+                let completions_guard = completions.read();
+
+                for (ix, mat) in matches[range].iter().enumerate() {
+                    let item_ix = start_ix + ix;
+                    let candidate_id = mat.candidate_id;
+                    let completion = &completions_guard[candidate_id];
+
+                    let documentation = if show_completion_documentation {
+                        &completion.documentation
+                    } else {
+                        &None
+                    };
+
+                    items.push(
+                        MouseEventHandler::new::<CompletionTag, _>(
+                            mat.candidate_id,
+                            cx,
+                            |state, _| {
+                                let item_style = if item_ix == selected_item {
+                                    style.autocomplete.selected_item
+                                } else if state.hovered() {
+                                    style.autocomplete.hovered_item
+                                } else {
+                                    style.autocomplete.item
+                                };
+
+                                let completion_label =
+                                    Text::new(completion.label.text.clone(), style.text.clone())
+                                        .with_soft_wrap(false)
+                                        .with_highlights(
+                                            combine_syntax_and_fuzzy_match_highlights(
+                                                &completion.label.text,
+                                                style.text.color.into(),
+                                                styled_runs_for_code_label(
+                                                    &completion.label,
+                                                    &style.syntax,
+                                                ),
+                                                &mat.positions,
+                                            ),
+                                        );
+
+                                if let Some(Documentation::SingleLine(text)) = documentation {
+                                    Flex::row()
+                                        .with_child(completion_label)
+                                        .with_children((|| {
+                                            let text_style = TextStyle {
+                                                color: style.autocomplete.inline_docs_color,
+                                                font_size: style.text.font_size
+                                                    * style.autocomplete.inline_docs_size_percent,
+                                                ..style.text.clone()
+                                            };
+
+                                            let label = Text::new(text.clone(), text_style)
+                                                .aligned()
+                                                .constrained()
+                                                .dynamically(move |constraint, _, _| {
+                                                    gpui::SizeConstraint {
+                                                        min: constraint.min,
+                                                        max: vec2f(
+                                                            constraint.max.x(),
+                                                            constraint.min.y(),
+                                                        ),
+                                                    }
+                                                });
+
+                                            if Some(item_ix) == widest_completion_ix {
+                                                Some(
+                                                    label
+                                                        .contained()
+                                                        .with_style(
+                                                            style
+                                                                .autocomplete
+                                                                .inline_docs_container,
+                                                        )
+                                                        .into_any(),
+                                                )
+                                            } else {
+                                                Some(label.flex_float().into_any())
+                                            }
+                                        })())
+                                        .into_any()
+                                } else {
+                                    completion_label.into_any()
+                                }
+                                .contained()
+                                .with_style(item_style)
+                                .constrained()
+                                .dynamically(
+                                    move |constraint, _, _| {
+                                        if Some(item_ix) == widest_completion_ix {
+                                            constraint
+                                        } else {
+                                            gpui::SizeConstraint {
+                                                min: constraint.min,
+                                                max: constraint.min,
+                                            }
+                                        }
+                                    },
+                                )
+                            },
+                        )
+                        .with_cursor_style(CursorStyle::PointingHand)
+                        .on_down(MouseButton::Left, move |_, this, cx| {
+                            this.confirm_completion(
+                                &ConfirmCompletion {
+                                    item_ix: Some(item_ix),
+                                },
+                                cx,
+                            )
+                            .map(|task| task.detach());
+                        })
+                        .constrained()
+                        .with_min_width(style.autocomplete.completion_min_width)
+                        .with_max_width(style.autocomplete.completion_max_width)
+                        .into_any(),
+                    );
+                }
+            }
+        })
+        .with_width_from_item(widest_completion_ix);
+
+        enum MultiLineDocumentation {}
+
+        Flex::row()
+            .with_child(list.flex(1., false))
+            .with_children({
+                let mat = &self.matches[selected_item];
+                let completions = self.completions.read();
+                let completion = &completions[mat.candidate_id];
+                let documentation = &completion.documentation;
+
+                match documentation {
+                    Some(Documentation::MultiLinePlainText(text)) => Some(
+                        Flex::column()
+                            .scrollable::<MultiLineDocumentation>(0, None, cx)
+                            .with_child(
+                                Text::new(text.clone(), style.text.clone()).with_soft_wrap(true),
+                            )
+                            .contained()
+                            .with_style(style.autocomplete.alongside_docs_container)
+                            .constrained()
+                            .with_max_width(style.autocomplete.alongside_docs_max_width)
+                            .flex(1., false),
+                    ),
+
+                    Some(Documentation::MultiLineMarkdown(parsed)) => Some(
+                        Flex::column()
+                            .scrollable::<MultiLineDocumentation>(0, None, cx)
+                            .with_child(render_parsed_markdown::<MultiLineDocumentation>(
+                                parsed, &style, workspace, cx,
+                            ))
+                            .contained()
+                            .with_style(style.autocomplete.alongside_docs_container)
+                            .constrained()
+                            .with_max_width(style.autocomplete.alongside_docs_max_width)
+                            .flex(1., false),
+                    ),
+
+                    _ => None,
+                }
+            })
+            .contained()
+            .with_style(style.autocomplete.container)
+            .into_any()
+    }
+
+    pub async fn filter(&mut self, query: Option<&str>, executor: Arc<executor::Background>) {
+        let mut matches = if let Some(query) = query {
+            fuzzy::match_strings(
+                &self.match_candidates,
+                query,
+                query.chars().any(|c| c.is_uppercase()),
+                100,
+                &Default::default(),
+                executor,
+            )
+            .await
+        } else {
+            self.match_candidates
+                .iter()
+                .enumerate()
+                .map(|(candidate_id, candidate)| StringMatch {
+                    candidate_id,
+                    score: Default::default(),
+                    positions: Default::default(),
+                    string: candidate.string.clone(),
+                })
+                .collect()
+        };
+
+        // Remove all candidates where the query's start does not match the start of any word in the candidate
+        if let Some(query) = query {
+            if let Some(query_start) = query.chars().next() {
+                matches.retain(|string_match| {
+                    split_words(&string_match.string).any(|word| {
+                        // Check that the first codepoint of the word as lowercase matches the first
+                        // codepoint of the query as lowercase
+                        word.chars()
+                            .flat_map(|codepoint| codepoint.to_lowercase())
+                            .zip(query_start.to_lowercase())
+                            .all(|(word_cp, query_cp)| word_cp == query_cp)
+                    })
+                });
+            }
+        }
+
+        let completions = self.completions.read();
+        matches.sort_unstable_by_key(|mat| {
+            let completion = &completions[mat.candidate_id];
+            (
+                completion.lsp_completion.sort_text.as_ref(),
+                Reverse(OrderedFloat(mat.score)),
+                completion.sort_key(),
+            )
+        });
+        drop(completions);
+
+        for mat in &mut matches {
+            let completions = self.completions.read();
+            let filter_start = completions[mat.candidate_id].label.filter_range.start;
+            for position in &mut mat.positions {
+                *position += filter_start;
+            }
+        }
+
+        self.matches = matches.into();
+        self.selected_item = 0;
+    }
+}
+
+#[derive(Clone)]
+struct CodeActionsMenu {
+    actions: Arc<[CodeAction]>,
+    buffer: ModelHandle<Buffer>,
+    selected_item: usize,
+    list: UniformListState,
+    deployed_from_indicator: bool,
+}
+
+impl CodeActionsMenu {
+    fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
+        self.selected_item = 0;
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        cx.notify()
+    }
+
+    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+        if self.selected_item > 0 {
+            self.selected_item -= 1;
+        } else {
+            self.selected_item = self.actions.len() - 1;
+        }
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        cx.notify();
+    }
+
+    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+        if self.selected_item + 1 < self.actions.len() {
+            self.selected_item += 1;
+        } else {
+            self.selected_item = 0;
+        }
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        cx.notify();
+    }
+
+    fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
+        self.selected_item = self.actions.len() - 1;
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        cx.notify()
+    }
+
+    fn visible(&self) -> bool {
+        !self.actions.is_empty()
+    }
+
+    fn render(
+        &self,
+        mut cursor_position: DisplayPoint,
+        style: EditorStyle,
+        cx: &mut ViewContext<Editor>,
+    ) -> (DisplayPoint, AnyElement<Editor>) {
+        enum ActionTag {}
+
+        let container_style = style.autocomplete.container;
+        let actions = self.actions.clone();
+        let selected_item = self.selected_item;
+        let element = UniformList::new(
+            self.list.clone(),
+            actions.len(),
+            cx,
+            move |_, range, items, cx| {
+                let start_ix = range.start;
+                for (ix, action) in actions[range].iter().enumerate() {
+                    let item_ix = start_ix + ix;
+                    items.push(
+                        MouseEventHandler::new::<ActionTag, _>(item_ix, cx, |state, _| {
+                            let item_style = if item_ix == selected_item {
+                                style.autocomplete.selected_item
+                            } else if state.hovered() {
+                                style.autocomplete.hovered_item
+                            } else {
+                                style.autocomplete.item
+                            };
+
+                            Text::new(action.lsp_action.title.clone(), style.text.clone())
+                                .with_soft_wrap(false)
+                                .contained()
+                                .with_style(item_style)
+                        })
+                        .with_cursor_style(CursorStyle::PointingHand)
+                        .on_down(MouseButton::Left, move |_, this, cx| {
+                            let workspace = this
+                                .workspace
+                                .as_ref()
+                                .and_then(|(workspace, _)| workspace.upgrade(cx));
+                            cx.window_context().defer(move |cx| {
+                                if let Some(workspace) = workspace {
+                                    workspace.update(cx, |workspace, cx| {
+                                        if let Some(task) = Editor::confirm_code_action(
+                                            workspace,
+                                            &ConfirmCodeAction {
+                                                item_ix: Some(item_ix),
+                                            },
+                                            cx,
+                                        ) {
+                                            task.detach_and_log_err(cx);
+                                        }
+                                    });
+                                }
+                            });
+                        })
+                        .into_any(),
+                    );
+                }
+            },
+        )
+        .with_width_from_item(
+            self.actions
+                .iter()
+                .enumerate()
+                .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
+                .map(|(ix, _)| ix),
+        )
+        .contained()
+        .with_style(container_style)
+        .into_any();
+
+        if self.deployed_from_indicator {
+            *cursor_position.column_mut() = 0;
+        }
+
+        (cursor_position, element)
+    }
+}
+
+pub struct CopilotState {
+    excerpt_id: Option<ExcerptId>,
+    pending_refresh: Task<Option<()>>,
+    pending_cycling_refresh: Task<Option<()>>,
+    cycled: bool,
+    completions: Vec<copilot::Completion>,
+    active_completion_index: usize,
+    suggestion: Option<Inlay>,
+}
+
+impl Default for CopilotState {
+    fn default() -> Self {
+        Self {
+            excerpt_id: None,
+            pending_cycling_refresh: Task::ready(Some(())),
+            pending_refresh: Task::ready(Some(())),
+            completions: Default::default(),
+            active_completion_index: 0,
+            cycled: false,
+            suggestion: None,
+        }
+    }
+}
+
+impl CopilotState {
+    fn active_completion(&self) -> Option<&copilot::Completion> {
+        self.completions.get(self.active_completion_index)
+    }
+
+    fn text_for_active_completion(
+        &self,
+        cursor: Anchor,
+        buffer: &MultiBufferSnapshot,
+    ) -> Option<&str> {
+        use language::ToOffset as _;
+
+        let completion = self.active_completion()?;
+        let excerpt_id = self.excerpt_id?;
+        let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
+        if excerpt_id != cursor.excerpt_id
+            || !completion.range.start.is_valid(completion_buffer)
+            || !completion.range.end.is_valid(completion_buffer)
+        {
+            return None;
+        }
+
+        let mut completion_range = completion.range.to_offset(&completion_buffer);
+        let prefix_len = Self::common_prefix(
+            completion_buffer.chars_for_range(completion_range.clone()),
+            completion.text.chars(),
+        );
+        completion_range.start += prefix_len;
+        let suffix_len = Self::common_prefix(
+            completion_buffer.reversed_chars_for_range(completion_range.clone()),
+            completion.text[prefix_len..].chars().rev(),
+        );
+        completion_range.end = completion_range.end.saturating_sub(suffix_len);
+
+        if completion_range.is_empty()
+            && completion_range.start == cursor.text_anchor.to_offset(&completion_buffer)
+        {
+            Some(&completion.text[prefix_len..completion.text.len() - suffix_len])
+        } else {
+            None
+        }
+    }
+
+    fn cycle_completions(&mut self, direction: Direction) {
+        match direction {
+            Direction::Prev => {
+                self.active_completion_index = if self.active_completion_index == 0 {
+                    self.completions.len().saturating_sub(1)
+                } else {
+                    self.active_completion_index - 1
+                };
+            }
+            Direction::Next => {
+                if self.completions.len() == 0 {
+                    self.active_completion_index = 0
+                } else {
+                    self.active_completion_index =
+                        (self.active_completion_index + 1) % self.completions.len();
+                }
+            }
+        }
+    }
+
+    fn push_completion(&mut self, new_completion: copilot::Completion) {
+        for completion in &self.completions {
+            if completion.text == new_completion.text && completion.range == new_completion.range {
+                return;
+            }
+        }
+        self.completions.push(new_completion);
+    }
+
+    fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
+        a.zip(b)
+            .take_while(|(a, b)| a == b)
+            .map(|(a, _)| a.len_utf8())
+            .sum()
+    }
+}
+
+#[derive(Debug)]
+struct ActiveDiagnosticGroup {
+    primary_range: Range<Anchor>,
+    primary_message: String,
+    blocks: HashMap<BlockId, Diagnostic>,
+    is_valid: bool,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct ClipboardSelection {
+    pub len: usize,
+    pub is_entire_line: bool,
+    pub first_line_indent: u32,
+}
+
+#[derive(Debug)]
+pub struct NavigationData {
+    cursor_anchor: Anchor,
+    cursor_position: Point,
+    scroll_anchor: ScrollAnchor,
+    scroll_top_row: u32,
+}
+
+pub struct EditorCreated(pub ViewHandle<Editor>);
+
+enum GotoDefinitionKind {
+    Symbol,
+    Type,
+}
+
+#[derive(Debug, Clone)]
+enum InlayHintRefreshReason {
+    Toggle(bool),
+    SettingsChange(InlayHintSettings),
+    NewLinesShown,
+    BufferEdited(HashSet<Arc<Language>>),
+    RefreshRequested,
+    ExcerptsRemoved(Vec<ExcerptId>),
+}
+impl InlayHintRefreshReason {
+    fn description(&self) -> &'static str {
+        match self {
+            Self::Toggle(_) => "toggle",
+            Self::SettingsChange(_) => "settings change",
+            Self::NewLinesShown => "new lines shown",
+            Self::BufferEdited(_) => "buffer edited",
+            Self::RefreshRequested => "refresh requested",
+            Self::ExcerptsRemoved(_) => "excerpts removed",
+        }
+    }
+}
+
+impl Editor {
+    pub fn single_line(
+        field_editor_style: Option<Arc<GetFieldEditorTheme>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx)
+    }
+
+    pub fn multi_line(
+        field_editor_style: Option<Arc<GetFieldEditorTheme>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        Self::new(EditorMode::Full, buffer, None, field_editor_style, cx)
+    }
+
+    pub fn auto_height(
+        max_lines: usize,
+        field_editor_style: Option<Arc<GetFieldEditorTheme>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        Self::new(
+            EditorMode::AutoHeight { max_lines },
+            buffer,
+            None,
+            field_editor_style,
+            cx,
+        )
+    }
+
+    pub fn for_buffer(
+        buffer: ModelHandle<Buffer>,
+        project: Option<ModelHandle<Project>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        Self::new(EditorMode::Full, buffer, project, None, cx)
+    }
+
+    pub fn for_multibuffer(
+        buffer: ModelHandle<MultiBuffer>,
+        project: Option<ModelHandle<Project>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        Self::new(EditorMode::Full, buffer, project, None, cx)
+    }
+
+    pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
+        let mut clone = Self::new(
+            self.mode,
+            self.buffer.clone(),
+            self.project.clone(),
+            self.get_field_editor_theme.clone(),
+            cx,
+        );
+        self.display_map.update(cx, |display_map, cx| {
+            let snapshot = display_map.snapshot(cx);
+            clone.display_map.update(cx, |display_map, cx| {
+                display_map.set_state(&snapshot, cx);
+            });
+        });
+        clone.selections.clone_state(&self.selections);
+        clone.scroll_manager.clone_state(&self.scroll_manager);
+        clone.searchable = self.searchable;
+        clone
+    }
+
+    fn new(
+        mode: EditorMode,
+        buffer: ModelHandle<MultiBuffer>,
+        project: Option<ModelHandle<Project>>,
+        get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let editor_view_id = cx.view_id();
+        let display_map = cx.add_model(|cx| {
+            let settings = settings::get::<ThemeSettings>(cx);
+            let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx);
+            DisplayMap::new(
+                buffer.clone(),
+                style.text.font_id,
+                style.text.font_size,
+                None,
+                2,
+                1,
+                cx,
+            )
+        });
+
+        let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
+
+        let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
+
+        let soft_wrap_mode_override =
+            (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
+
+        let mut project_subscriptions = Vec::new();
+        if mode == EditorMode::Full {
+            if let Some(project) = project.as_ref() {
+                if buffer.read(cx).is_singleton() {
+                    project_subscriptions.push(cx.observe(project, |_, _, cx| {
+                        cx.emit(Event::TitleChanged);
+                    }));
+                }
+                project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
+                    if let project::Event::RefreshInlayHints = event {
+                        editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
+                    };
+                }));
+            }
+        }
+
+        let inlay_hint_settings = inlay_hint_settings(
+            selections.newest_anchor().head(),
+            &buffer.read(cx).snapshot(cx),
+            cx,
+        );
+
+        let mut this = Self {
+            handle: cx.weak_handle(),
+            buffer: buffer.clone(),
+            display_map: display_map.clone(),
+            selections,
+            scroll_manager: ScrollManager::new(),
+            columnar_selection_tail: None,
+            add_selections_state: None,
+            select_next_state: None,
+            select_prev_state: None,
+            selection_history: Default::default(),
+            autoclose_regions: Default::default(),
+            snippet_stack: Default::default(),
+            select_larger_syntax_node_stack: Vec::new(),
+            ime_transaction: Default::default(),
+            active_diagnostics: None,
+            soft_wrap_mode_override,
+            get_field_editor_theme,
+            collaboration_hub: project.clone().map(|project| Box::new(project) as _),
+            project,
+            focused: false,
+            blink_manager: blink_manager.clone(),
+            show_local_selections: true,
+            mode,
+            show_gutter: mode == EditorMode::Full,
+            show_wrap_guides: None,
+            placeholder_text: None,
+            highlighted_rows: None,
+            background_highlights: Default::default(),
+            inlay_background_highlights: Default::default(),
+            nav_history: None,
+            context_menu: RwLock::new(None),
+            mouse_context_menu: cx
+                .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
+            completion_tasks: Default::default(),
+            next_completion_id: 0,
+            next_inlay_id: 0,
+            available_code_actions: Default::default(),
+            code_actions_task: Default::default(),
+            document_highlights_task: Default::default(),
+            pending_rename: Default::default(),
+            searchable: true,
+            override_text_style: None,
+            cursor_shape: Default::default(),
+            autoindent_mode: Some(AutoindentMode::EachLine),
+            collapse_matches: false,
+            workspace: None,
+            keymap_context_layers: Default::default(),
+            input_enabled: true,
+            read_only: false,
+            leader_peer_id: None,
+            remote_id: None,
+            hover_state: Default::default(),
+            link_go_to_definition_state: Default::default(),
+            copilot_state: Default::default(),
+            inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
+            gutter_hovered: false,
+            pixel_position_of_newest_cursor: None,
+            _subscriptions: vec![
+                cx.observe(&buffer, Self::on_buffer_changed),
+                cx.subscribe(&buffer, Self::on_buffer_event),
+                cx.observe(&display_map, Self::on_display_map_changed),
+                cx.observe(&blink_manager, |_, _, cx| cx.notify()),
+                cx.observe_global::<SettingsStore, _>(Self::settings_changed),
+                cx.observe_window_activation(|editor, active, cx| {
+                    editor.blink_manager.update(cx, |blink_manager, cx| {
+                        if active {
+                            blink_manager.enable(cx);
+                        } else {
+                            blink_manager.show_cursor(cx);
+                            blink_manager.disable(cx);
+                        }
+                    });
+                }),
+            ],
+        };
+
+        this._subscriptions.extend(project_subscriptions);
+
+        this.end_selection(cx);
+        this.scroll_manager.show_scrollbar(cx);
+
+        let editor_created_event = EditorCreated(cx.handle());
+        cx.emit_global(editor_created_event);
+
+        if mode == EditorMode::Full {
+            let should_auto_hide_scrollbars = cx.platform().should_auto_hide_scrollbars();
+            cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
+        }
+
+        this.report_editor_event("open", None, cx);
+        this
+    }
+
+    pub fn new_file(
+        workspace: &mut Workspace,
+        _: &workspace::NewFile,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let project = workspace.project().clone();
+        if project.read(cx).is_remote() {
+            cx.propagate_action();
+        } else if let Some(buffer) = project
+            .update(cx, |project, cx| project.create_buffer("", None, cx))
+            .log_err()
+        {
+            workspace.add_item(
+                Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
+                cx,
+            );
+        }
+    }
+
+    pub fn new_file_in_direction(
+        workspace: &mut Workspace,
+        action: &workspace::NewFileInDirection,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let project = workspace.project().clone();
+        if project.read(cx).is_remote() {
+            cx.propagate_action();
+        } else if let Some(buffer) = project
+            .update(cx, |project, cx| project.create_buffer("", None, cx))
+            .log_err()
+        {
+            workspace.split_item(
+                action.0,
+                Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
+                cx,
+            );
+        }
+    }
+
+    pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
+        self.buffer.read(cx).replica_id()
+    }
+
+    pub fn leader_peer_id(&self) -> Option<PeerId> {
+        self.leader_peer_id
+    }
+
+    pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
+        &self.buffer
+    }
+
+    fn workspace(&self, cx: &AppContext) -> Option<ViewHandle<Workspace>> {
+        self.workspace.as_ref()?.0.upgrade(cx)
+    }
+
+    pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> {
+        self.buffer().read(cx).title(cx)
+    }
+
+    pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot {
+        EditorSnapshot {
+            mode: self.mode,
+            show_gutter: self.show_gutter,
+            display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
+            scroll_anchor: self.scroll_manager.anchor(),
+            ongoing_scroll: self.scroll_manager.ongoing_scroll(),
+            placeholder_text: self.placeholder_text.clone(),
+            is_focused: self
+                .handle
+                .upgrade(cx)
+                .map_or(false, |handle| handle.is_focused(cx)),
+        }
+    }
+
+    pub fn language_at<'a, T: ToOffset>(
+        &self,
+        point: T,
+        cx: &'a AppContext,
+    ) -> Option<Arc<Language>> {
+        self.buffer.read(cx).language_at(point, cx)
+    }
+
+    pub fn file_at<'a, T: ToOffset>(&self, point: T, cx: &'a AppContext) -> Option<Arc<dyn File>> {
+        self.buffer.read(cx).read(cx).file_at(point).cloned()
+    }
+
+    pub fn active_excerpt(
+        &self,
+        cx: &AppContext,
+    ) -> Option<(ExcerptId, ModelHandle<Buffer>, Range<text::Anchor>)> {
+        self.buffer
+            .read(cx)
+            .excerpt_containing(self.selections.newest_anchor().head(), cx)
+    }
+
+    pub fn style(&self, cx: &AppContext) -> EditorStyle {
+        build_style(
+            settings::get::<ThemeSettings>(cx),
+            self.get_field_editor_theme.as_deref(),
+            self.override_text_style.as_deref(),
+            cx,
+        )
+    }
+
+    pub fn mode(&self) -> EditorMode {
+        self.mode
+    }
+
+    pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
+        self.collaboration_hub.as_deref()
+    }
+
+    pub fn set_collaboration_hub(&mut self, hub: Box<dyn CollaborationHub>) {
+        self.collaboration_hub = Some(hub);
+    }
+
+    pub fn set_placeholder_text(
+        &mut self,
+        placeholder_text: impl Into<Arc<str>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.placeholder_text = Some(placeholder_text.into());
+        cx.notify();
+    }
+
+    pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
+        self.cursor_shape = cursor_shape;
+        cx.notify();
+    }
+
+    pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
+        self.collapse_matches = collapse_matches;
+    }
+
+    pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
+        if self.collapse_matches {
+            return range.start..range.start;
+        }
+        range.clone()
+    }
+
+    pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
+        if self.display_map.read(cx).clip_at_line_ends != clip {
+            self.display_map
+                .update(cx, |map, _| map.clip_at_line_ends = clip);
+        }
+    }
+
+    pub fn set_keymap_context_layer<Tag: 'static>(
+        &mut self,
+        context: KeymapContext,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.keymap_context_layers
+            .insert(TypeId::of::<Tag>(), context);
+        cx.notify();
+    }
+
+    pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
+        self.keymap_context_layers.remove(&TypeId::of::<Tag>());
+        cx.notify();
+    }
+
+    pub fn set_input_enabled(&mut self, input_enabled: bool) {
+        self.input_enabled = input_enabled;
+    }
+
+    pub fn set_autoindent(&mut self, autoindent: bool) {
+        if autoindent {
+            self.autoindent_mode = Some(AutoindentMode::EachLine);
+        } else {
+            self.autoindent_mode = None;
+        }
+    }
+
+    pub fn read_only(&self) -> bool {
+        self.read_only
+    }
+
+    pub fn set_read_only(&mut self, read_only: bool) {
+        self.read_only = read_only;
+    }
+
+    pub fn set_field_editor_style(
+        &mut self,
+        style: Option<Arc<GetFieldEditorTheme>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.get_field_editor_theme = style;
+        cx.notify();
+    }
+
+    fn selections_did_change(
+        &mut self,
+        local: bool,
+        old_cursor_position: &Anchor,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if self.focused && self.leader_peer_id.is_none() {
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.set_active_selections(
+                    &self.selections.disjoint_anchors(),
+                    self.selections.line_mode,
+                    self.cursor_shape,
+                    cx,
+                )
+            });
+        }
+
+        let display_map = self
+            .display_map
+            .update(cx, |display_map, cx| display_map.snapshot(cx));
+        let buffer = &display_map.buffer_snapshot;
+        self.add_selections_state = None;
+        self.select_next_state = None;
+        self.select_prev_state = None;
+        self.select_larger_syntax_node_stack.clear();
+        self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
+        self.snippet_stack
+            .invalidate(&self.selections.disjoint_anchors(), buffer);
+        self.take_rename(false, cx);
+
+        let new_cursor_position = self.selections.newest_anchor().head();
+
+        self.push_to_nav_history(
+            old_cursor_position.clone(),
+            Some(new_cursor_position.to_point(buffer)),
+            cx,
+        );
+
+        if local {
+            let new_cursor_position = self.selections.newest_anchor().head();
+            let mut context_menu = self.context_menu.write();
+            let completion_menu = match context_menu.as_ref() {
+                Some(ContextMenu::Completions(menu)) => Some(menu),
+
+                _ => {
+                    *context_menu = None;
+                    None
+                }
+            };
+
+            if let Some(completion_menu) = completion_menu {
+                let cursor_position = new_cursor_position.to_offset(buffer);
+                let (word_range, kind) =
+                    buffer.surrounding_word(completion_menu.initial_position.clone());
+                if kind == Some(CharKind::Word)
+                    && word_range.to_inclusive().contains(&cursor_position)
+                {
+                    let mut completion_menu = completion_menu.clone();
+                    drop(context_menu);
+
+                    let query = Self::completion_query(buffer, cursor_position);
+                    cx.spawn(move |this, mut cx| async move {
+                        completion_menu
+                            .filter(query.as_deref(), cx.background().clone())
+                            .await;
+
+                        this.update(&mut cx, |this, cx| {
+                            let mut context_menu = this.context_menu.write();
+                            let Some(ContextMenu::Completions(menu)) = context_menu.as_ref() else {
+                                return;
+                            };
+
+                            if menu.id > completion_menu.id {
+                                return;
+                            }
+
+                            *context_menu = Some(ContextMenu::Completions(completion_menu));
+                            drop(context_menu);
+                            cx.notify();
+                        })
+                    })
+                    .detach();
+
+                    self.show_completions(&ShowCompletions, cx);
+                } else {
+                    drop(context_menu);
+                    self.hide_context_menu(cx);
+                }
+            } else {
+                drop(context_menu);
+            }
+
+            hide_hover(self, cx);
+
+            if old_cursor_position.to_display_point(&display_map).row()
+                != new_cursor_position.to_display_point(&display_map).row()
+            {
+                self.available_code_actions.take();
+            }
+            self.refresh_code_actions(cx);
+            self.refresh_document_highlights(cx);
+            refresh_matching_bracket_highlights(self, cx);
+            self.discard_copilot_suggestion(cx);
+        }
+
+        self.blink_manager.update(cx, BlinkManager::pause_blinking);
+        cx.emit(Event::SelectionsChanged { local });
+        cx.notify();
+    }
+
+    pub fn change_selections<R>(
+        &mut self,
+        autoscroll: Option<Autoscroll>,
+        cx: &mut ViewContext<Self>,
+        change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
+    ) -> R {
+        let old_cursor_position = self.selections.newest_anchor().head();
+        self.push_to_selection_history();
+
+        let (changed, result) = self.selections.change_with(cx, change);
+
+        if changed {
+            if let Some(autoscroll) = autoscroll {
+                self.request_autoscroll(autoscroll, cx);
+            }
+            self.selections_did_change(true, &old_cursor_position, cx);
+        }
+
+        result
+    }
+
+    pub fn edit<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
+    where
+        I: IntoIterator<Item = (Range<S>, T)>,
+        S: ToOffset,
+        T: Into<Arc<str>>,
+    {
+        if self.read_only {
+            return;
+        }
+
+        self.buffer
+            .update(cx, |buffer, cx| buffer.edit(edits, None, cx));
+    }
+
+    pub fn edit_with_autoindent<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
+    where
+        I: IntoIterator<Item = (Range<S>, T)>,
+        S: ToOffset,
+        T: Into<Arc<str>>,
+    {
+        if self.read_only {
+            return;
+        }
+
+        self.buffer.update(cx, |buffer, cx| {
+            buffer.edit(edits, self.autoindent_mode.clone(), cx)
+        });
+    }
+
+    pub fn edit_with_block_indent<I, S, T>(
+        &mut self,
+        edits: I,
+        original_indent_columns: Vec<u32>,
+        cx: &mut ViewContext<Self>,
+    ) where
+        I: IntoIterator<Item = (Range<S>, T)>,
+        S: ToOffset,
+        T: Into<Arc<str>>,
+    {
+        if self.read_only {
+            return;
+        }
+
+        self.buffer.update(cx, |buffer, cx| {
+            buffer.edit(
+                edits,
+                Some(AutoindentMode::Block {
+                    original_indent_columns,
+                }),
+                cx,
+            )
+        });
+    }
+
+    fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext<Self>) {
+        self.hide_context_menu(cx);
+
+        match phase {
+            SelectPhase::Begin {
+                position,
+                add,
+                click_count,
+            } => self.begin_selection(position, add, click_count, cx),
+            SelectPhase::BeginColumnar {
+                position,
+                goal_column,
+            } => self.begin_columnar_selection(position, goal_column, cx),
+            SelectPhase::Extend {
+                position,
+                click_count,
+            } => self.extend_selection(position, click_count, cx),
+            SelectPhase::Update {
+                position,
+                goal_column,
+                scroll_position,
+            } => self.update_selection(position, goal_column, scroll_position, cx),
+            SelectPhase::End => self.end_selection(cx),
+        }
+    }
+
+    fn extend_selection(
+        &mut self,
+        position: DisplayPoint,
+        click_count: usize,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let tail = self.selections.newest::<usize>(cx).tail();
+        self.begin_selection(position, false, click_count, cx);
+
+        let position = position.to_offset(&display_map, Bias::Left);
+        let tail_anchor = display_map.buffer_snapshot.anchor_before(tail);
+
+        let mut pending_selection = self
+            .selections
+            .pending_anchor()
+            .expect("extend_selection not called with pending selection");
+        if position >= tail {
+            pending_selection.start = tail_anchor;
+        } else {
+            pending_selection.end = tail_anchor;
+            pending_selection.reversed = true;
+        }
+
+        let mut pending_mode = self.selections.pending_mode().unwrap();
+        match &mut pending_mode {
+            SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor,
+            _ => {}
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.set_pending(pending_selection, pending_mode)
+        });
+    }
+
+    fn begin_selection(
+        &mut self,
+        position: DisplayPoint,
+        add: bool,
+        click_count: usize,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if !self.focused {
+            cx.focus_self();
+        }
+
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = &display_map.buffer_snapshot;
+        let newest_selection = self.selections.newest_anchor().clone();
+        let position = display_map.clip_point(position, Bias::Left);
+
+        let start;
+        let end;
+        let mode;
+        let auto_scroll;
+        match click_count {
+            1 => {
+                start = buffer.anchor_before(position.to_point(&display_map));
+                end = start.clone();
+                mode = SelectMode::Character;
+                auto_scroll = true;
+            }
+            2 => {
+                let range = movement::surrounding_word(&display_map, position);
+                start = buffer.anchor_before(range.start.to_point(&display_map));
+                end = buffer.anchor_before(range.end.to_point(&display_map));
+                mode = SelectMode::Word(start.clone()..end.clone());
+                auto_scroll = true;
+            }
+            3 => {
+                let position = display_map
+                    .clip_point(position, Bias::Left)
+                    .to_point(&display_map);
+                let line_start = display_map.prev_line_boundary(position).0;
+                let next_line_start = buffer.clip_point(
+                    display_map.next_line_boundary(position).0 + Point::new(1, 0),
+                    Bias::Left,
+                );
+                start = buffer.anchor_before(line_start);
+                end = buffer.anchor_before(next_line_start);
+                mode = SelectMode::Line(start.clone()..end.clone());
+                auto_scroll = true;
+            }
+            _ => {
+                start = buffer.anchor_before(0);
+                end = buffer.anchor_before(buffer.len());
+                mode = SelectMode::All;
+                auto_scroll = false;
+            }
+        }
+
+        self.change_selections(auto_scroll.then(|| Autoscroll::newest()), cx, |s| {
+            if !add {
+                s.clear_disjoint();
+            } else if click_count > 1 {
+                s.delete(newest_selection.id)
+            }
+
+            s.set_pending_anchor_range(start..end, mode);
+        });
+    }
+
+    fn begin_columnar_selection(
+        &mut self,
+        position: DisplayPoint,
+        goal_column: u32,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if !self.focused {
+            cx.focus_self();
+        }
+
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let tail = self.selections.newest::<Point>(cx).tail();
+        self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail));
+
+        self.select_columns(
+            tail.to_display_point(&display_map),
+            position,
+            goal_column,
+            &display_map,
+            cx,
+        );
+    }
+
+    fn update_selection(
+        &mut self,
+        position: DisplayPoint,
+        goal_column: u32,
+        scroll_position: Vector2F,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        if let Some(tail) = self.columnar_selection_tail.as_ref() {
+            let tail = tail.to_display_point(&display_map);
+            self.select_columns(tail, position, goal_column, &display_map, cx);
+        } else if let Some(mut pending) = self.selections.pending_anchor() {
+            let buffer = self.buffer.read(cx).snapshot(cx);
+            let head;
+            let tail;
+            let mode = self.selections.pending_mode().unwrap();
+            match &mode {
+                SelectMode::Character => {
+                    head = position.to_point(&display_map);
+                    tail = pending.tail().to_point(&buffer);
+                }
+                SelectMode::Word(original_range) => {
+                    let original_display_range = original_range.start.to_display_point(&display_map)
+                        ..original_range.end.to_display_point(&display_map);
+                    let original_buffer_range = original_display_range.start.to_point(&display_map)
+                        ..original_display_range.end.to_point(&display_map);
+                    if movement::is_inside_word(&display_map, position)
+                        || original_display_range.contains(&position)
+                    {
+                        let word_range = movement::surrounding_word(&display_map, position);
+                        if word_range.start < original_display_range.start {
+                            head = word_range.start.to_point(&display_map);
+                        } else {
+                            head = word_range.end.to_point(&display_map);
+                        }
+                    } else {
+                        head = position.to_point(&display_map);
+                    }
+
+                    if head <= original_buffer_range.start {
+                        tail = original_buffer_range.end;
+                    } else {
+                        tail = original_buffer_range.start;
+                    }
+                }
+                SelectMode::Line(original_range) => {
+                    let original_range = original_range.to_point(&display_map.buffer_snapshot);
+
+                    let position = display_map
+                        .clip_point(position, Bias::Left)
+                        .to_point(&display_map);
+                    let line_start = display_map.prev_line_boundary(position).0;
+                    let next_line_start = buffer.clip_point(
+                        display_map.next_line_boundary(position).0 + Point::new(1, 0),
+                        Bias::Left,
+                    );
+
+                    if line_start < original_range.start {
+                        head = line_start
+                    } else {
+                        head = next_line_start
+                    }
+
+                    if head <= original_range.start {
+                        tail = original_range.end;
+                    } else {
+                        tail = original_range.start;
+                    }
+                }
+                SelectMode::All => {
+                    return;
+                }
+            };
+
+            if head < tail {
+                pending.start = buffer.anchor_before(head);
+                pending.end = buffer.anchor_before(tail);
+                pending.reversed = true;
+            } else {
+                pending.start = buffer.anchor_before(tail);
+                pending.end = buffer.anchor_before(head);
+                pending.reversed = false;
+            }
+
+            self.change_selections(None, cx, |s| {
+                s.set_pending(pending, mode);
+            });
+        } else {
+            error!("update_selection dispatched with no pending selection");
+            return;
+        }
+
+        self.set_scroll_position(scroll_position, cx);
+        cx.notify();
+    }
+
+    fn end_selection(&mut self, cx: &mut ViewContext<Self>) {
+        self.columnar_selection_tail.take();
+        if self.selections.pending_anchor().is_some() {
+            let selections = self.selections.all::<usize>(cx);
+            self.change_selections(None, cx, |s| {
+                s.select(selections);
+                s.clear_pending();
+            });
+        }
+    }
+
+    fn select_columns(
+        &mut self,
+        tail: DisplayPoint,
+        head: DisplayPoint,
+        goal_column: u32,
+        display_map: &DisplaySnapshot,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let start_row = cmp::min(tail.row(), head.row());
+        let end_row = cmp::max(tail.row(), head.row());
+        let start_column = cmp::min(tail.column(), goal_column);
+        let end_column = cmp::max(tail.column(), goal_column);
+        let reversed = start_column < tail.column();
+
+        let selection_ranges = (start_row..=end_row)
+            .filter_map(|row| {
+                if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) {
+                    let start = display_map
+                        .clip_point(DisplayPoint::new(row, start_column), Bias::Left)
+                        .to_point(display_map);
+                    let end = display_map
+                        .clip_point(DisplayPoint::new(row, end_column), Bias::Right)
+                        .to_point(display_map);
+                    if reversed {
+                        Some(end..start)
+                    } else {
+                        Some(start..end)
+                    }
+                } else {
+                    None
+                }
+            })
+            .collect::<Vec<_>>();
+
+        self.change_selections(None, cx, |s| {
+            s.select_ranges(selection_ranges);
+        });
+        cx.notify();
+    }
+
+    pub fn has_pending_nonempty_selection(&self) -> bool {
+        let pending_nonempty_selection = match self.selections.pending_anchor() {
+            Some(Selection { start, end, .. }) => start != end,
+            None => false,
+        };
+        pending_nonempty_selection || self.columnar_selection_tail.is_some()
+    }
+
+    pub fn has_pending_selection(&self) -> bool {
+        self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some()
+    }
+
+    pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+        if self.take_rename(false, cx).is_some() {
+            return;
+        }
+
+        if hide_hover(self, cx) {
+            return;
+        }
+
+        if self.hide_context_menu(cx).is_some() {
+            return;
+        }
+
+        if self.discard_copilot_suggestion(cx) {
+            return;
+        }
+
+        if self.snippet_stack.pop().is_some() {
+            return;
+        }
+
+        if self.mode == EditorMode::Full {
+            if self.active_diagnostics.is_some() {
+                self.dismiss_diagnostics(cx);
+                return;
+            }
+
+            if self.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel()) {
+                return;
+            }
+        }
+
+        cx.propagate_action();
+    }
+
+    pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
+        let text: Arc<str> = text.into();
+
+        if self.read_only {
+            return;
+        }
+
+        let selections = self.selections.all_adjusted(cx);
+        let mut brace_inserted = false;
+        let mut edits = Vec::new();
+        let mut new_selections = Vec::with_capacity(selections.len());
+        let mut new_autoclose_regions = Vec::new();
+        let snapshot = self.buffer.read(cx).read(cx);
+
+        for (selection, autoclose_region) in
+            self.selections_with_autoclose_regions(selections, &snapshot)
+        {
+            if let Some(scope) = snapshot.language_scope_at(selection.head()) {
+                // Determine if the inserted text matches the opening or closing
+                // bracket of any of this language's bracket pairs.
+                let mut bracket_pair = None;
+                let mut is_bracket_pair_start = false;
+                if !text.is_empty() {
+                    // `text` can be empty when an user is using IME (e.g. Chinese Wubi Simplified)
+                    //  and they are removing the character that triggered IME popup.
+                    for (pair, enabled) in scope.brackets() {
+                        if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
+                            bracket_pair = Some(pair.clone());
+                            is_bracket_pair_start = true;
+                            break;
+                        } else if pair.end.as_str() == text.as_ref() {
+                            bracket_pair = Some(pair.clone());
+                            break;
+                        }
+                    }
+                }
+
+                if let Some(bracket_pair) = bracket_pair {
+                    if selection.is_empty() {
+                        if is_bracket_pair_start {
+                            let prefix_len = bracket_pair.start.len() - text.len();
+
+                            // If the inserted text is a suffix of an opening bracket and the
+                            // selection is preceded by the rest of the opening bracket, then
+                            // insert the closing bracket.
+                            let following_text_allows_autoclose = snapshot
+                                .chars_at(selection.start)
+                                .next()
+                                .map_or(true, |c| scope.should_autoclose_before(c));
+                            let preceding_text_matches_prefix = prefix_len == 0
+                                || (selection.start.column >= (prefix_len as u32)
+                                    && snapshot.contains_str_at(
+                                        Point::new(
+                                            selection.start.row,
+                                            selection.start.column - (prefix_len as u32),
+                                        ),
+                                        &bracket_pair.start[..prefix_len],
+                                    ));
+                            if following_text_allows_autoclose && preceding_text_matches_prefix {
+                                let anchor = snapshot.anchor_before(selection.end);
+                                new_selections.push((selection.map(|_| anchor), text.len()));
+                                new_autoclose_regions.push((
+                                    anchor,
+                                    text.len(),
+                                    selection.id,
+                                    bracket_pair.clone(),
+                                ));
+                                edits.push((
+                                    selection.range(),
+                                    format!("{}{}", text, bracket_pair.end).into(),
+                                ));
+                                brace_inserted = true;
+                                continue;
+                            }
+                        }
+
+                        if let Some(region) = autoclose_region {
+                            // If the selection is followed by an auto-inserted closing bracket,
+                            // then don't insert that closing bracket again; just move the selection
+                            // past the closing bracket.
+                            let should_skip = selection.end == region.range.end.to_point(&snapshot)
+                                && text.as_ref() == region.pair.end.as_str();
+                            if should_skip {
+                                let anchor = snapshot.anchor_after(selection.end);
+                                new_selections
+                                    .push((selection.map(|_| anchor), region.pair.end.len()));
+                                continue;
+                            }
+                        }
+                    }
+                    // If an opening bracket is 1 character long and is typed while
+                    // text is selected, then surround that text with the bracket pair.
+                    else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 {
+                        edits.push((selection.start..selection.start, text.clone()));
+                        edits.push((
+                            selection.end..selection.end,
+                            bracket_pair.end.as_str().into(),
+                        ));
+                        brace_inserted = true;
+                        new_selections.push((
+                            Selection {
+                                id: selection.id,
+                                start: snapshot.anchor_after(selection.start),
+                                end: snapshot.anchor_before(selection.end),
+                                reversed: selection.reversed,
+                                goal: selection.goal,
+                            },
+                            0,
+                        ));
+                        continue;
+                    }
+                }
+            }
+
+            // If not handling any auto-close operation, then just replace the selected
+            // text with the given input and move the selection to the end of the
+            // newly inserted text.
+            let anchor = snapshot.anchor_after(selection.end);
+            new_selections.push((selection.map(|_| anchor), 0));
+            edits.push((selection.start..selection.end, text.clone()));
+        }
+
+        drop(snapshot);
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, this.autoindent_mode.clone(), cx);
+            });
+
+            let new_anchor_selections = new_selections.iter().map(|e| &e.0);
+            let new_selection_deltas = new_selections.iter().map(|e| e.1);
+            let snapshot = this.buffer.read(cx).read(cx);
+            let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &snapshot)
+                .zip(new_selection_deltas)
+                .map(|(selection, delta)| Selection {
+                    id: selection.id,
+                    start: selection.start + delta,
+                    end: selection.end + delta,
+                    reversed: selection.reversed,
+                    goal: SelectionGoal::None,
+                })
+                .collect::<Vec<_>>();
+
+            let mut i = 0;
+            for (position, delta, selection_id, pair) in new_autoclose_regions {
+                let position = position.to_offset(&snapshot) + delta;
+                let start = snapshot.anchor_before(position);
+                let end = snapshot.anchor_after(position);
+                while let Some(existing_state) = this.autoclose_regions.get(i) {
+                    match existing_state.range.start.cmp(&start, &snapshot) {
+                        Ordering::Less => i += 1,
+                        Ordering::Greater => break,
+                        Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) {
+                            Ordering::Less => i += 1,
+                            Ordering::Equal => break,
+                            Ordering::Greater => break,
+                        },
+                    }
+                }
+                this.autoclose_regions.insert(
+                    i,
+                    AutocloseRegion {
+                        selection_id,
+                        range: start..end,
+                        pair,
+                    },
+                );
+            }
+
+            drop(snapshot);
+            let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
+
+            if !brace_inserted && settings::get::<EditorSettings>(cx).use_on_type_format {
+                if let Some(on_type_format_task) =
+                    this.trigger_on_type_formatting(text.to_string(), cx)
+                {
+                    on_type_format_task.detach_and_log_err(cx);
+                }
+            }
+
+            if had_active_copilot_suggestion {
+                this.refresh_copilot_suggestions(true, cx);
+                if !this.has_active_copilot_suggestion(cx) {
+                    this.trigger_completion_on_input(&text, cx);
+                }
+            } else {
+                this.trigger_completion_on_input(&text, cx);
+                this.refresh_copilot_suggestions(true, cx);
+            }
+        });
+    }
+
+    pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext<Self>) {
+        self.transact(cx, |this, cx| {
+            let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
+                let selections = this.selections.all::<usize>(cx);
+                let multi_buffer = this.buffer.read(cx);
+                let buffer = multi_buffer.snapshot(cx);
+                selections
+                    .iter()
+                    .map(|selection| {
+                        let start_point = selection.start.to_point(&buffer);
+                        let mut indent = buffer.indent_size_for_line(start_point.row);
+                        indent.len = cmp::min(indent.len, start_point.column);
+                        let start = selection.start;
+                        let end = selection.end;
+                        let is_cursor = start == end;
+                        let language_scope = buffer.language_scope_at(start);
+                        let (comment_delimiter, insert_extra_newline) = if let Some(language) =
+                            &language_scope
+                        {
+                            let leading_whitespace_len = buffer
+                                .reversed_chars_at(start)
+                                .take_while(|c| c.is_whitespace() && *c != '\n')
+                                .map(|c| c.len_utf8())
+                                .sum::<usize>();
+
+                            let trailing_whitespace_len = buffer
+                                .chars_at(end)
+                                .take_while(|c| c.is_whitespace() && *c != '\n')
+                                .map(|c| c.len_utf8())
+                                .sum::<usize>();
+
+                            let insert_extra_newline =
+                                language.brackets().any(|(pair, enabled)| {
+                                    let pair_start = pair.start.trim_end();
+                                    let pair_end = pair.end.trim_start();
+
+                                    enabled
+                                        && pair.newline
+                                        && buffer.contains_str_at(
+                                            end + trailing_whitespace_len,
+                                            pair_end,
+                                        )
+                                        && buffer.contains_str_at(
+                                            (start - leading_whitespace_len)
+                                                .saturating_sub(pair_start.len()),
+                                            pair_start,
+                                        )
+                                });
+                            // Comment extension on newline is allowed only for cursor selections
+                            let comment_delimiter = language.line_comment_prefix().filter(|_| {
+                                let is_comment_extension_enabled =
+                                    multi_buffer.settings_at(0, cx).extend_comment_on_newline;
+                                is_cursor && is_comment_extension_enabled
+                            });
+                            let comment_delimiter = if let Some(delimiter) = comment_delimiter {
+                                buffer
+                                    .buffer_line_for_row(start_point.row)
+                                    .is_some_and(|(snapshot, range)| {
+                                        let mut index_of_first_non_whitespace = 0;
+                                        let line_starts_with_comment = snapshot
+                                            .chars_for_range(range)
+                                            .skip_while(|c| {
+                                                let should_skip = c.is_whitespace();
+                                                if should_skip {
+                                                    index_of_first_non_whitespace += 1;
+                                                }
+                                                should_skip
+                                            })
+                                            .take(delimiter.len())
+                                            .eq(delimiter.chars());
+                                        let cursor_is_placed_after_comment_marker =
+                                            index_of_first_non_whitespace + delimiter.len()
+                                                <= start_point.column as usize;
+                                        line_starts_with_comment
+                                            && cursor_is_placed_after_comment_marker
+                                    })
+                                    .then(|| delimiter.clone())
+                            } else {
+                                None
+                            };
+                            (comment_delimiter, insert_extra_newline)
+                        } else {
+                            (None, false)
+                        };
+
+                        let capacity_for_delimiter = comment_delimiter
+                            .as_deref()
+                            .map(str::len)
+                            .unwrap_or_default();
+                        let mut new_text =
+                            String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
+                        new_text.push_str("\n");
+                        new_text.extend(indent.chars());
+                        if let Some(delimiter) = &comment_delimiter {
+                            new_text.push_str(&delimiter);
+                        }
+                        if insert_extra_newline {
+                            new_text = new_text.repeat(2);
+                        }
+
+                        let anchor = buffer.anchor_after(end);
+                        let new_selection = selection.map(|_| anchor);
+                        (
+                            (start..end, new_text),
+                            (insert_extra_newline, new_selection),
+                        )
+                    })
+                    .unzip()
+            };
+
+            this.edit_with_autoindent(edits, cx);
+            let buffer = this.buffer.read(cx).snapshot(cx);
+            let new_selections = selection_fixup_info
+                .into_iter()
+                .map(|(extra_newline_inserted, new_selection)| {
+                    let mut cursor = new_selection.end.to_point(&buffer);
+                    if extra_newline_inserted {
+                        cursor.row -= 1;
+                        cursor.column = buffer.line_len(cursor.row);
+                    }
+                    new_selection.map(|_| cursor)
+                })
+                .collect();
+
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
+            this.refresh_copilot_suggestions(true, cx);
+        });
+    }
+
+    pub fn newline_above(&mut self, _: &NewlineAbove, cx: &mut ViewContext<Self>) {
+        let buffer = self.buffer.read(cx);
+        let snapshot = buffer.snapshot(cx);
+
+        let mut edits = Vec::new();
+        let mut rows = Vec::new();
+        let mut rows_inserted = 0;
+
+        for selection in self.selections.all_adjusted(cx) {
+            let cursor = selection.head();
+            let row = cursor.row;
+
+            let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left);
+
+            let newline = "\n".to_string();
+            edits.push((start_of_line..start_of_line, newline));
+
+            rows.push(row + rows_inserted);
+            rows_inserted += 1;
+        }
+
+        self.transact(cx, |editor, cx| {
+            editor.edit(edits, cx);
+
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                let mut index = 0;
+                s.move_cursors_with(|map, _, _| {
+                    let row = rows[index];
+                    index += 1;
+
+                    let point = Point::new(row, 0);
+                    let boundary = map.next_line_boundary(point).1;
+                    let clipped = map.clip_point(boundary, Bias::Left);
+
+                    (clipped, SelectionGoal::None)
+                });
+            });
+
+            let mut indent_edits = Vec::new();
+            let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+            for row in rows {
+                let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
+                for (row, indent) in indents {
+                    if indent.len == 0 {
+                        continue;
+                    }
+
+                    let text = match indent.kind {
+                        IndentKind::Space => " ".repeat(indent.len as usize),
+                        IndentKind::Tab => "\t".repeat(indent.len as usize),
+                    };
+                    let point = Point::new(row, 0);
+                    indent_edits.push((point..point, text));
+                }
+            }
+            editor.edit(indent_edits, cx);
+        });
+    }
+
+    pub fn newline_below(&mut self, _: &NewlineBelow, cx: &mut ViewContext<Self>) {
+        let buffer = self.buffer.read(cx);
+        let snapshot = buffer.snapshot(cx);
+
+        let mut edits = Vec::new();
+        let mut rows = Vec::new();
+        let mut rows_inserted = 0;
+
+        for selection in self.selections.all_adjusted(cx) {
+            let cursor = selection.head();
+            let row = cursor.row;
+
+            let point = Point::new(row + 1, 0);
+            let start_of_line = snapshot.clip_point(point, Bias::Left);
+
+            let newline = "\n".to_string();
+            edits.push((start_of_line..start_of_line, newline));
+
+            rows_inserted += 1;
+            rows.push(row + rows_inserted);
+        }
+
+        self.transact(cx, |editor, cx| {
+            editor.edit(edits, cx);
+
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                let mut index = 0;
+                s.move_cursors_with(|map, _, _| {
+                    let row = rows[index];
+                    index += 1;
+
+                    let point = Point::new(row, 0);
+                    let boundary = map.next_line_boundary(point).1;
+                    let clipped = map.clip_point(boundary, Bias::Left);
+
+                    (clipped, SelectionGoal::None)
+                });
+            });
+
+            let mut indent_edits = Vec::new();
+            let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+            for row in rows {
+                let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
+                for (row, indent) in indents {
+                    if indent.len == 0 {
+                        continue;
+                    }
+
+                    let text = match indent.kind {
+                        IndentKind::Space => " ".repeat(indent.len as usize),
+                        IndentKind::Tab => "\t".repeat(indent.len as usize),
+                    };
+                    let point = Point::new(row, 0);
+                    indent_edits.push((point..point, text));
+                }
+            }
+            editor.edit(indent_edits, cx);
+        });
+    }
+
+    pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
+        self.insert_with_autoindent_mode(
+            text,
+            Some(AutoindentMode::Block {
+                original_indent_columns: Vec::new(),
+            }),
+            cx,
+        );
+    }
+
+    fn insert_with_autoindent_mode(
+        &mut self,
+        text: &str,
+        autoindent_mode: Option<AutoindentMode>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if self.read_only {
+            return;
+        }
+
+        let text: Arc<str> = text.into();
+        self.transact(cx, |this, cx| {
+            let old_selections = this.selections.all_adjusted(cx);
+            let selection_anchors = this.buffer.update(cx, |buffer, cx| {
+                let anchors = {
+                    let snapshot = buffer.read(cx);
+                    old_selections
+                        .iter()
+                        .map(|s| {
+                            let anchor = snapshot.anchor_after(s.head());
+                            s.map(|_| anchor)
+                        })
+                        .collect::<Vec<_>>()
+                };
+                buffer.edit(
+                    old_selections
+                        .iter()
+                        .map(|s| (s.start..s.end, text.clone())),
+                    autoindent_mode,
+                    cx,
+                );
+                anchors
+            });
+
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select_anchors(selection_anchors);
+            })
+        });
+    }
+
+    fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
+        if !settings::get::<EditorSettings>(cx).show_completions_on_input {
+            return;
+        }
+
+        let selection = self.selections.newest_anchor();
+        if self
+            .buffer
+            .read(cx)
+            .is_completion_trigger(selection.head(), text, cx)
+        {
+            self.show_completions(&ShowCompletions, cx);
+        } else {
+            self.hide_context_menu(cx);
+        }
+    }
+
+    /// If any empty selections is touching the start of its innermost containing autoclose
+    /// region, expand it to select the brackets.
+    fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) {
+        let selections = self.selections.all::<usize>(cx);
+        let buffer = self.buffer.read(cx).read(cx);
+        let mut new_selections = Vec::new();
+        for (mut selection, region) in self.selections_with_autoclose_regions(selections, &buffer) {
+            if let (Some(region), true) = (region, selection.is_empty()) {
+                let mut range = region.range.to_offset(&buffer);
+                if selection.start == range.start {
+                    if range.start >= region.pair.start.len() {
+                        range.start -= region.pair.start.len();
+                        if buffer.contains_str_at(range.start, &region.pair.start) {
+                            if buffer.contains_str_at(range.end, &region.pair.end) {
+                                range.end += region.pair.end.len();
+                                selection.start = range.start;
+                                selection.end = range.end;
+                            }
+                        }
+                    }
+                }
+            }
+            new_selections.push(selection);
+        }
+
+        drop(buffer);
+        self.change_selections(None, cx, |selections| selections.select(new_selections));
+    }
+
+    /// Iterate the given selections, and for each one, find the smallest surrounding
+    /// autoclose region. This uses the ordering of the selections and the autoclose
+    /// regions to avoid repeated comparisons.
+    fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>(
+        &'a self,
+        selections: impl IntoIterator<Item = Selection<D>>,
+        buffer: &'a MultiBufferSnapshot,
+    ) -> impl Iterator<Item = (Selection<D>, Option<&'a AutocloseRegion>)> {
+        let mut i = 0;
+        let mut regions = self.autoclose_regions.as_slice();
+        selections.into_iter().map(move |selection| {
+            let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer);
+
+            let mut enclosing = None;
+            while let Some(pair_state) = regions.get(i) {
+                if pair_state.range.end.to_offset(buffer) < range.start {
+                    regions = &regions[i + 1..];
+                    i = 0;
+                } else if pair_state.range.start.to_offset(buffer) > range.end {
+                    break;
+                } else {
+                    if pair_state.selection_id == selection.id {
+                        enclosing = Some(pair_state);
+                    }
+                    i += 1;
+                }
+            }
+
+            (selection.clone(), enclosing)
+        })
+    }
+
+    /// Remove any autoclose regions that no longer contain their selection.
+    fn invalidate_autoclose_regions(
+        &mut self,
+        mut selections: &[Selection<Anchor>],
+        buffer: &MultiBufferSnapshot,
+    ) {
+        self.autoclose_regions.retain(|state| {
+            let mut i = 0;
+            while let Some(selection) = selections.get(i) {
+                if selection.end.cmp(&state.range.start, buffer).is_lt() {
+                    selections = &selections[1..];
+                    continue;
+                }
+                if selection.start.cmp(&state.range.end, buffer).is_gt() {
+                    break;
+                }
+                if selection.id == state.selection_id {
+                    return true;
+                } else {
+                    i += 1;
+                }
+            }
+            false
+        });
+    }
+
+    fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
+        let offset = position.to_offset(buffer);
+        let (word_range, kind) = buffer.surrounding_word(offset);
+        if offset > word_range.start && kind == Some(CharKind::Word) {
+            Some(
+                buffer
+                    .text_for_range(word_range.start..offset)
+                    .collect::<String>(),
+            )
+        } else {
+            None
+        }
+    }
+
+    pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext<Self>) {
+        self.refresh_inlay_hints(
+            InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled),
+            cx,
+        );
+    }
+
+    pub fn inlay_hints_enabled(&self) -> bool {
+        self.inlay_hint_cache.enabled
+    }
+
+    fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext<Self>) {
+        if self.project.is_none() || self.mode != EditorMode::Full {
+            return;
+        }
+
+        let reason_description = reason.description();
+        let (invalidate_cache, required_languages) = match reason {
+            InlayHintRefreshReason::Toggle(enabled) => {
+                self.inlay_hint_cache.enabled = enabled;
+                if enabled {
+                    (InvalidationStrategy::RefreshRequested, None)
+                } else {
+                    self.inlay_hint_cache.clear();
+                    self.splice_inlay_hints(
+                        self.visible_inlay_hints(cx)
+                            .iter()
+                            .map(|inlay| inlay.id)
+                            .collect(),
+                        Vec::new(),
+                        cx,
+                    );
+                    return;
+                }
+            }
+            InlayHintRefreshReason::SettingsChange(new_settings) => {
+                match self.inlay_hint_cache.update_settings(
+                    &self.buffer,
+                    new_settings,
+                    self.visible_inlay_hints(cx),
+                    cx,
+                ) {
+                    ControlFlow::Break(Some(InlaySplice {
+                        to_remove,
+                        to_insert,
+                    })) => {
+                        self.splice_inlay_hints(to_remove, to_insert, cx);
+                        return;
+                    }
+                    ControlFlow::Break(None) => return,
+                    ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
+                }
+            }
+            InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => {
+                if let Some(InlaySplice {
+                    to_remove,
+                    to_insert,
+                }) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
+                {
+                    self.splice_inlay_hints(to_remove, to_insert, cx);
+                }
+                return;
+            }
+            InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
+            InlayHintRefreshReason::BufferEdited(buffer_languages) => {
+                (InvalidationStrategy::BufferEdited, Some(buffer_languages))
+            }
+            InlayHintRefreshReason::RefreshRequested => {
+                (InvalidationStrategy::RefreshRequested, None)
+            }
+        };
+
+        if let Some(InlaySplice {
+            to_remove,
+            to_insert,
+        }) = self.inlay_hint_cache.spawn_hint_refresh(
+            reason_description,
+            self.excerpt_visible_offsets(required_languages.as_ref(), cx),
+            invalidate_cache,
+            cx,
+        ) {
+            self.splice_inlay_hints(to_remove, to_insert, cx);
+        }
+    }
+
+    fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec<Inlay> {
+        self.display_map
+            .read(cx)
+            .current_inlays()
+            .filter(move |inlay| {
+                Some(inlay.id) != self.copilot_state.suggestion.as_ref().map(|h| h.id)
+            })
+            .cloned()
+            .collect()
+    }
+
+    pub fn excerpt_visible_offsets(
+        &self,
+        restrict_to_languages: Option<&HashSet<Arc<Language>>>,
+        cx: &mut ViewContext<'_, '_, Editor>,
+    ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
+        let multi_buffer = self.buffer().read(cx);
+        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+        let multi_buffer_visible_start = self
+            .scroll_manager
+            .anchor()
+            .anchor
+            .to_point(&multi_buffer_snapshot);
+        let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
+            multi_buffer_visible_start
+                + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
+            Bias::Left,
+        );
+        let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
+        multi_buffer
+            .range_to_buffer_ranges(multi_buffer_visible_range, cx)
+            .into_iter()
+            .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
+            .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
+                let buffer = buffer_handle.read(cx);
+                let language = buffer.language()?;
+                if let Some(restrict_to_languages) = restrict_to_languages {
+                    if !restrict_to_languages.contains(language) {
+                        return None;
+                    }
+                }
+                Some((
+                    excerpt_id,
+                    (
+                        buffer_handle,
+                        buffer.version().clone(),
+                        excerpt_visible_range,
+                    ),
+                ))
+            })
+            .collect()
+    }
+
+    pub fn text_layout_details(&self, cx: &WindowContext) -> TextLayoutDetails {
+        TextLayoutDetails {
+            font_cache: cx.font_cache().clone(),
+            text_layout_cache: cx.text_layout_cache().clone(),
+            editor_style: self.style(cx),
+        }
+    }
+
+    fn splice_inlay_hints(
+        &self,
+        to_remove: Vec<InlayId>,
+        to_insert: Vec<Inlay>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.display_map.update(cx, |display_map, cx| {
+            display_map.splice_inlays(to_remove, to_insert, cx);
+        });
+        cx.notify();
+    }
+
+    fn trigger_on_type_formatting(
+        &self,
+        input: String,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if input.len() != 1 {
+            return None;
+        }
+
+        let project = self.project.as_ref()?;
+        let position = self.selections.newest_anchor().head();
+        let (buffer, buffer_position) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(position.clone(), cx)?;
+
+        // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances,
+        // hence we do LSP request & edit on host side only โ€”ย add formats to host's history.
+        let push_to_lsp_host_history = true;
+        // If this is not the host, append its history with new edits.
+        let push_to_client_history = project.read(cx).is_remote();
+
+        let on_type_formatting = project.update(cx, |project, cx| {
+            project.on_type_format(
+                buffer.clone(),
+                buffer_position,
+                input,
+                push_to_lsp_host_history,
+                cx,
+            )
+        });
+        Some(cx.spawn(|editor, mut cx| async move {
+            if let Some(transaction) = on_type_formatting.await? {
+                if push_to_client_history {
+                    buffer.update(&mut cx, |buffer, _| {
+                        buffer.push_transaction(transaction, Instant::now());
+                    });
+                }
+                editor.update(&mut cx, |editor, cx| {
+                    editor.refresh_document_highlights(cx);
+                })?;
+            }
+            Ok(())
+        }))
+    }
+
+    fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
+        if self.pending_rename.is_some() {
+            return;
+        }
+
+        let project = if let Some(project) = self.project.clone() {
+            project
+        } else {
+            return;
+        };
+
+        let position = self.selections.newest_anchor().head();
+        let (buffer, buffer_position) = if let Some(output) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(position.clone(), cx)
+        {
+            output
+        } else {
+            return;
+        };
+
+        let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone());
+        let completions = project.update(cx, |project, cx| {
+            project.completions(&buffer, buffer_position, cx)
+        });
+
+        let id = post_inc(&mut self.next_completion_id);
+        let task = cx.spawn(|this, mut cx| {
+            async move {
+                let menu = if let Some(completions) = completions.await.log_err() {
+                    let mut menu = CompletionsMenu {
+                        id,
+                        initial_position: position,
+                        match_candidates: completions
+                            .iter()
+                            .enumerate()
+                            .map(|(id, completion)| {
+                                StringMatchCandidate::new(
+                                    id,
+                                    completion.label.text[completion.label.filter_range.clone()]
+                                        .into(),
+                                )
+                            })
+                            .collect(),
+                        buffer,
+                        completions: Arc::new(RwLock::new(completions.into())),
+                        matches: Vec::new().into(),
+                        selected_item: 0,
+                        list: Default::default(),
+                    };
+                    menu.filter(query.as_deref(), cx.background()).await;
+                    if menu.matches.is_empty() {
+                        None
+                    } else {
+                        _ = this.update(&mut cx, |editor, cx| {
+                            menu.pre_resolve_completion_documentation(editor.project.clone(), cx);
+                        });
+                        Some(menu)
+                    }
+                } else {
+                    None
+                };
+
+                this.update(&mut cx, |this, cx| {
+                    this.completion_tasks.retain(|(task_id, _)| *task_id > id);
+
+                    let mut context_menu = this.context_menu.write();
+                    match context_menu.as_ref() {
+                        None => {}
+
+                        Some(ContextMenu::Completions(prev_menu)) => {
+                            if prev_menu.id > id {
+                                return;
+                            }
+                        }
+
+                        _ => return,
+                    }
+
+                    if this.focused && menu.is_some() {
+                        let menu = menu.unwrap();
+                        *context_menu = Some(ContextMenu::Completions(menu));
+                        drop(context_menu);
+                        this.discard_copilot_suggestion(cx);
+                        cx.notify();
+                    } else if this.completion_tasks.is_empty() {
+                        // If there are no more completion tasks and the last menu was
+                        // empty, we should hide it. If it was already hidden, we should
+                        // also show the copilot suggestion when available.
+                        drop(context_menu);
+                        if this.hide_context_menu(cx).is_none() {
+                            this.update_visible_copilot_suggestion(cx);
+                        }
+                    }
+                })?;
+
+                Ok::<_, anyhow::Error>(())
+            }
+            .log_err()
+        });
+        self.completion_tasks.push((id, task));
+    }
+
+    pub fn confirm_completion(
+        &mut self,
+        action: &ConfirmCompletion,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        use language::ToOffset as _;
+
+        let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
+            menu
+        } else {
+            return None;
+        };
+
+        let mat = completions_menu
+            .matches
+            .get(action.item_ix.unwrap_or(completions_menu.selected_item))?;
+        let buffer_handle = completions_menu.buffer;
+        let completions = completions_menu.completions.read();
+        let completion = completions.get(mat.candidate_id)?;
+
+        let snippet;
+        let text;
+        if completion.is_snippet() {
+            snippet = Some(Snippet::parse(&completion.new_text).log_err()?);
+            text = snippet.as_ref().unwrap().text.clone();
+        } else {
+            snippet = None;
+            text = completion.new_text.clone();
+        };
+        let selections = self.selections.all::<usize>(cx);
+        let buffer = buffer_handle.read(cx);
+        let old_range = completion.old_range.to_offset(buffer);
+        let old_text = buffer.text_for_range(old_range.clone()).collect::<String>();
+
+        let newest_selection = self.selections.newest_anchor();
+        if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
+            return None;
+        }
+
+        let lookbehind = newest_selection
+            .start
+            .text_anchor
+            .to_offset(buffer)
+            .saturating_sub(old_range.start);
+        let lookahead = old_range
+            .end
+            .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer));
+        let mut common_prefix_len = old_text
+            .bytes()
+            .zip(text.bytes())
+            .take_while(|(a, b)| a == b)
+            .count();
+
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let mut range_to_replace: Option<Range<isize>> = None;
+        let mut ranges = Vec::new();
+        for selection in &selections {
+            if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
+                let start = selection.start.saturating_sub(lookbehind);
+                let end = selection.end + lookahead;
+                if selection.id == newest_selection.id {
+                    range_to_replace = Some(
+                        ((start + common_prefix_len) as isize - selection.start as isize)
+                            ..(end as isize - selection.start as isize),
+                    );
+                }
+                ranges.push(start + common_prefix_len..end);
+            } else {
+                common_prefix_len = 0;
+                ranges.clear();
+                ranges.extend(selections.iter().map(|s| {
+                    if s.id == newest_selection.id {
+                        range_to_replace = Some(
+                            old_range.start.to_offset_utf16(&snapshot).0 as isize
+                                - selection.start as isize
+                                ..old_range.end.to_offset_utf16(&snapshot).0 as isize
+                                    - selection.start as isize,
+                        );
+                        old_range.clone()
+                    } else {
+                        s.start..s.end
+                    }
+                }));
+                break;
+            }
+        }
+        let text = &text[common_prefix_len..];
+
+        cx.emit(Event::InputHandled {
+            utf16_range_to_replace: range_to_replace,
+            text: text.into(),
+        });
+
+        self.transact(cx, |this, cx| {
+            if let Some(mut snippet) = snippet {
+                snippet.text = text.to_string();
+                for tabstop in snippet.tabstops.iter_mut().flatten() {
+                    tabstop.start -= common_prefix_len as isize;
+                    tabstop.end -= common_prefix_len as isize;
+                }
+
+                this.insert_snippet(&ranges, snippet, cx).log_err();
+            } else {
+                this.buffer.update(cx, |buffer, cx| {
+                    buffer.edit(
+                        ranges.iter().map(|range| (range.clone(), text)),
+                        this.autoindent_mode.clone(),
+                        cx,
+                    );
+                });
+            }
+
+            this.refresh_copilot_suggestions(true, cx);
+        });
+
+        let project = self.project.clone()?;
+        let apply_edits = project.update(cx, |project, cx| {
+            project.apply_additional_edits_for_completion(
+                buffer_handle,
+                completion.clone(),
+                true,
+                cx,
+            )
+        });
+        Some(cx.foreground().spawn(async move {
+            apply_edits.await?;
+            Ok(())
+        }))
+    }
+
+    pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
+        let mut context_menu = self.context_menu.write();
+        if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
+            *context_menu = None;
+            cx.notify();
+            return;
+        }
+        drop(context_menu);
+
+        let deployed_from_indicator = action.deployed_from_indicator;
+        let mut task = self.code_actions_task.take();
+        cx.spawn(|this, mut cx| async move {
+            while let Some(prev_task) = task {
+                prev_task.await;
+                task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
+            }
+
+            this.update(&mut cx, |this, cx| {
+                if this.focused {
+                    if let Some((buffer, actions)) = this.available_code_actions.clone() {
+                        this.completion_tasks.clear();
+                        this.discard_copilot_suggestion(cx);
+                        *this.context_menu.write() =
+                            Some(ContextMenu::CodeActions(CodeActionsMenu {
+                                buffer,
+                                actions,
+                                selected_item: Default::default(),
+                                list: Default::default(),
+                                deployed_from_indicator,
+                            }));
+                    }
+                }
+            })?;
+
+            Ok::<_, anyhow::Error>(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    pub fn confirm_code_action(
+        workspace: &mut Workspace,
+        action: &ConfirmCodeAction,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Task<Result<()>>> {
+        let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
+        let actions_menu = if let ContextMenu::CodeActions(menu) =
+            editor.update(cx, |editor, cx| editor.hide_context_menu(cx))?
+        {
+            menu
+        } else {
+            return None;
+        };
+        let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
+        let action = actions_menu.actions.get(action_ix)?.clone();
+        let title = action.lsp_action.title.clone();
+        let buffer = actions_menu.buffer;
+
+        let apply_code_actions = workspace.project().clone().update(cx, |project, cx| {
+            project.apply_code_action(buffer, action, true, cx)
+        });
+        let editor = editor.downgrade();
+        Some(cx.spawn(|workspace, cx| async move {
+            let project_transaction = apply_code_actions.await?;
+            Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await
+        }))
+    }
+
+    async fn open_project_transaction(
+        this: &WeakViewHandle<Editor>,
+        workspace: WeakViewHandle<Workspace>,
+        transaction: ProjectTransaction,
+        title: String,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx))?;
+
+        let mut entries = transaction.0.into_iter().collect::<Vec<_>>();
+        entries.sort_unstable_by_key(|(buffer, _)| {
+            buffer.read_with(&cx, |buffer, _| buffer.file().map(|f| f.path().clone()))
+        });
+
+        // If the project transaction's edits are all contained within this editor, then
+        // avoid opening a new editor to display them.
+
+        if let Some((buffer, transaction)) = entries.first() {
+            if entries.len() == 1 {
+                let excerpt = this.read_with(&cx, |editor, cx| {
+                    editor
+                        .buffer()
+                        .read(cx)
+                        .excerpt_containing(editor.selections.newest_anchor().head(), cx)
+                })?;
+                if let Some((_, excerpted_buffer, excerpt_range)) = excerpt {
+                    if excerpted_buffer == *buffer {
+                        let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| {
+                            let excerpt_range = excerpt_range.to_offset(buffer);
+                            buffer
+                                .edited_ranges_for_transaction::<usize>(transaction)
+                                .all(|range| {
+                                    excerpt_range.start <= range.start
+                                        && excerpt_range.end >= range.end
+                                })
+                        });
+
+                        if all_edits_within_excerpt {
+                            return Ok(());
+                        }
+                    }
+                }
+            }
+        } else {
+            return Ok(());
+        }
+
+        let mut ranges_to_highlight = Vec::new();
+        let excerpt_buffer = cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(replica_id).with_title(title);
+            for (buffer_handle, transaction) in &entries {
+                let buffer = buffer_handle.read(cx);
+                ranges_to_highlight.extend(
+                    multibuffer.push_excerpts_with_context_lines(
+                        buffer_handle.clone(),
+                        buffer
+                            .edited_ranges_for_transaction::<usize>(transaction)
+                            .collect(),
+                        1,
+                        cx,
+                    ),
+                );
+            }
+            multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx);
+            multibuffer
+        });
+
+        workspace.update(&mut cx, |workspace, cx| {
+            let project = workspace.project().clone();
+            let editor =
+                cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
+            workspace.add_item(Box::new(editor.clone()), cx);
+            editor.update(cx, |editor, cx| {
+                editor.highlight_background::<Self>(
+                    ranges_to_highlight,
+                    |theme| theme.editor.highlighted_line_background,
+                    cx,
+                );
+            });
+        })?;
+
+        Ok(())
+    }
+
+    fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+        let project = self.project.clone()?;
+        let buffer = self.buffer.read(cx);
+        let newest_selection = self.selections.newest_anchor().clone();
+        let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?;
+        let (end_buffer, end) = buffer.text_anchor_for_position(newest_selection.end, cx)?;
+        if start_buffer != end_buffer {
+            return None;
+        }
+
+        self.code_actions_task = Some(cx.spawn(|this, mut cx| async move {
+            cx.background().timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT).await;
+
+            let actions = project
+                .update(&mut cx, |project, cx| {
+                    project.code_actions(&start_buffer, start..end, cx)
+                })
+                .await;
+
+            this.update(&mut cx, |this, cx| {
+                this.available_code_actions = actions.log_err().and_then(|actions| {
+                    if actions.is_empty() {
+                        None
+                    } else {
+                        Some((start_buffer, actions.into()))
+                    }
+                });
+                cx.notify();
+            })
+            .log_err();
+        }));
+        None
+    }
+
+    fn refresh_document_highlights(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+        if self.pending_rename.is_some() {
+            return None;
+        }
+
+        let project = self.project.clone()?;
+        let buffer = self.buffer.read(cx);
+        let newest_selection = self.selections.newest_anchor().clone();
+        let cursor_position = newest_selection.head();
+        let (cursor_buffer, cursor_buffer_position) =
+            buffer.text_anchor_for_position(cursor_position.clone(), cx)?;
+        let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?;
+        if cursor_buffer != tail_buffer {
+            return None;
+        }
+
+        self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move {
+            cx.background()
+                .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT)
+                .await;
+
+            let highlights = project
+                .update(&mut cx, |project, cx| {
+                    project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
+                })
+                .await
+                .log_err();
+
+            if let Some(highlights) = highlights {
+                this.update(&mut cx, |this, cx| {
+                    if this.pending_rename.is_some() {
+                        return;
+                    }
+
+                    let buffer_id = cursor_position.buffer_id;
+                    let buffer = this.buffer.read(cx);
+                    if !buffer
+                        .text_anchor_for_position(cursor_position, cx)
+                        .map_or(false, |(buffer, _)| buffer == cursor_buffer)
+                    {
+                        return;
+                    }
+
+                    let cursor_buffer_snapshot = cursor_buffer.read(cx);
+                    let mut write_ranges = Vec::new();
+                    let mut read_ranges = Vec::new();
+                    for highlight in highlights {
+                        for (excerpt_id, excerpt_range) in
+                            buffer.excerpts_for_buffer(&cursor_buffer, cx)
+                        {
+                            let start = highlight
+                                .range
+                                .start
+                                .max(&excerpt_range.context.start, cursor_buffer_snapshot);
+                            let end = highlight
+                                .range
+                                .end
+                                .min(&excerpt_range.context.end, cursor_buffer_snapshot);
+                            if start.cmp(&end, cursor_buffer_snapshot).is_ge() {
+                                continue;
+                            }
+
+                            let range = Anchor {
+                                buffer_id,
+                                excerpt_id: excerpt_id.clone(),
+                                text_anchor: start,
+                            }..Anchor {
+                                buffer_id,
+                                excerpt_id,
+                                text_anchor: end,
+                            };
+                            if highlight.kind == lsp::DocumentHighlightKind::WRITE {
+                                write_ranges.push(range);
+                            } else {
+                                read_ranges.push(range);
+                            }
+                        }
+                    }
+
+                    this.highlight_background::<DocumentHighlightRead>(
+                        read_ranges,
+                        |theme| theme.editor.document_highlight_read_background,
+                        cx,
+                    );
+                    this.highlight_background::<DocumentHighlightWrite>(
+                        write_ranges,
+                        |theme| theme.editor.document_highlight_write_background,
+                        cx,
+                    );
+                    cx.notify();
+                })
+                .log_err();
+            }
+        }));
+        None
+    }
+
+    fn refresh_copilot_suggestions(
+        &mut self,
+        debounce: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<()> {
+        let copilot = Copilot::global(cx)?;
+        if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
+            self.clear_copilot_suggestions(cx);
+            return None;
+        }
+        self.update_visible_copilot_suggestion(cx);
+
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let cursor = self.selections.newest_anchor().head();
+        if !self.is_copilot_enabled_at(cursor, &snapshot, cx) {
+            self.clear_copilot_suggestions(cx);
+            return None;
+        }
+
+        let (buffer, buffer_position) =
+            self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
+        self.copilot_state.pending_refresh = cx.spawn(|this, mut cx| async move {
+            if debounce {
+                cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
+            }
+
+            let completions = copilot
+                .update(&mut cx, |copilot, cx| {
+                    copilot.completions(&buffer, buffer_position, cx)
+                })
+                .await
+                .log_err()
+                .into_iter()
+                .flatten()
+                .collect_vec();
+
+            this.update(&mut cx, |this, cx| {
+                if !completions.is_empty() {
+                    this.copilot_state.cycled = false;
+                    this.copilot_state.pending_cycling_refresh = Task::ready(None);
+                    this.copilot_state.completions.clear();
+                    this.copilot_state.active_completion_index = 0;
+                    this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
+                    for completion in completions {
+                        this.copilot_state.push_completion(completion);
+                    }
+                    this.update_visible_copilot_suggestion(cx);
+                }
+            })
+            .log_err()?;
+            Some(())
+        });
+
+        Some(())
+    }
+
+    fn cycle_copilot_suggestions(
+        &mut self,
+        direction: Direction,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<()> {
+        let copilot = Copilot::global(cx)?;
+        if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
+            return None;
+        }
+
+        if self.copilot_state.cycled {
+            self.copilot_state.cycle_completions(direction);
+            self.update_visible_copilot_suggestion(cx);
+        } else {
+            let cursor = self.selections.newest_anchor().head();
+            let (buffer, buffer_position) =
+                self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
+            self.copilot_state.pending_cycling_refresh = cx.spawn(|this, mut cx| async move {
+                let completions = copilot
+                    .update(&mut cx, |copilot, cx| {
+                        copilot.completions_cycling(&buffer, buffer_position, cx)
+                    })
+                    .await;
+
+                this.update(&mut cx, |this, cx| {
+                    this.copilot_state.cycled = true;
+                    for completion in completions.log_err().into_iter().flatten() {
+                        this.copilot_state.push_completion(completion);
+                    }
+                    this.copilot_state.cycle_completions(direction);
+                    this.update_visible_copilot_suggestion(cx);
+                })
+                .log_err()?;
+
+                Some(())
+            });
+        }
+
+        Some(())
+    }
+
+    fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext<Self>) {
+        if !self.has_active_copilot_suggestion(cx) {
+            self.refresh_copilot_suggestions(false, cx);
+            return;
+        }
+
+        self.update_visible_copilot_suggestion(cx);
+    }
+
+    fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
+        if self.has_active_copilot_suggestion(cx) {
+            self.cycle_copilot_suggestions(Direction::Next, cx);
+        } else {
+            let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none();
+            if is_copilot_disabled {
+                cx.propagate_action();
+            }
+        }
+    }
+
+    fn previous_copilot_suggestion(
+        &mut self,
+        _: &copilot::PreviousSuggestion,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if self.has_active_copilot_suggestion(cx) {
+            self.cycle_copilot_suggestions(Direction::Prev, cx);
+        } else {
+            let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none();
+            if is_copilot_disabled {
+                cx.propagate_action();
+            }
+        }
+    }
+
+    fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
+            if let Some((copilot, completion)) =
+                Copilot::global(cx).zip(self.copilot_state.active_completion())
+            {
+                copilot
+                    .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
+                    .detach_and_log_err(cx);
+
+                self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
+            }
+            cx.emit(Event::InputHandled {
+                utf16_range_to_replace: None,
+                text: suggestion.text.to_string().into(),
+            });
+            self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
+            cx.notify();
+            true
+        } else {
+            false
+        }
+    }
+
+    fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
+            if let Some(copilot) = Copilot::global(cx) {
+                copilot
+                    .update(cx, |copilot, cx| {
+                        copilot.discard_completions(&self.copilot_state.completions, cx)
+                    })
+                    .detach_and_log_err(cx);
+
+                self.report_copilot_event(None, false, cx)
+            }
+
+            self.display_map.update(cx, |map, cx| {
+                map.splice_inlays(vec![suggestion.id], Vec::new(), cx)
+            });
+            cx.notify();
+            true
+        } else {
+            false
+        }
+    }
+
+    fn is_copilot_enabled_at(
+        &self,
+        location: Anchor,
+        snapshot: &MultiBufferSnapshot,
+        cx: &mut ViewContext<Self>,
+    ) -> bool {
+        let file = snapshot.file_at(location);
+        let language = snapshot.language_at(location);
+        let settings = all_language_settings(file, cx);
+        settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
+    }
+
+    fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
+        if let Some(suggestion) = self.copilot_state.suggestion.as_ref() {
+            let buffer = self.buffer.read(cx).read(cx);
+            suggestion.position.is_valid(&buffer)
+        } else {
+            false
+        }
+    }
+
+    fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
+        let suggestion = self.copilot_state.suggestion.take()?;
+        self.display_map.update(cx, |map, cx| {
+            map.splice_inlays(vec![suggestion.id], Default::default(), cx);
+        });
+        let buffer = self.buffer.read(cx).read(cx);
+
+        if suggestion.position.is_valid(&buffer) {
+            Some(suggestion)
+        } else {
+            None
+        }
+    }
+
+    fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let selection = self.selections.newest_anchor();
+        let cursor = selection.head();
+
+        if self.context_menu.read().is_some()
+            || !self.completion_tasks.is_empty()
+            || selection.start != selection.end
+        {
+            self.discard_copilot_suggestion(cx);
+        } else if let Some(text) = self
+            .copilot_state
+            .text_for_active_completion(cursor, &snapshot)
+        {
+            let text = Rope::from(text);
+            let mut to_remove = Vec::new();
+            if let Some(suggestion) = self.copilot_state.suggestion.take() {
+                to_remove.push(suggestion.id);
+            }
+
+            let suggestion_inlay =
+                Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
+            self.copilot_state.suggestion = Some(suggestion_inlay.clone());
+            self.display_map.update(cx, move |map, cx| {
+                map.splice_inlays(to_remove, vec![suggestion_inlay], cx)
+            });
+            cx.notify();
+        } else {
+            self.discard_copilot_suggestion(cx);
+        }
+    }
+
+    fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
+        self.copilot_state = Default::default();
+        self.discard_copilot_suggestion(cx);
+    }
+
+    pub fn render_code_actions_indicator(
+        &self,
+        style: &EditorStyle,
+        is_active: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<AnyElement<Self>> {
+        if self.available_code_actions.is_some() {
+            enum CodeActions {}
+            Some(
+                MouseEventHandler::new::<CodeActions, _>(0, cx, |state, _| {
+                    Svg::new("icons/bolt.svg").with_color(
+                        style
+                            .code_actions
+                            .indicator
+                            .in_state(is_active)
+                            .style_for(state)
+                            .color,
+                    )
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .with_padding(Padding::uniform(3.))
+                .on_down(MouseButton::Left, |_, this, cx| {
+                    this.toggle_code_actions(
+                        &ToggleCodeActions {
+                            deployed_from_indicator: true,
+                        },
+                        cx,
+                    );
+                })
+                .into_any(),
+            )
+        } else {
+            None
+        }
+    }
+
+    pub fn render_fold_indicators(
+        &self,
+        fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
+        style: &EditorStyle,
+        gutter_hovered: bool,
+        line_height: f32,
+        gutter_margin: f32,
+        cx: &mut ViewContext<Self>,
+    ) -> Vec<Option<AnyElement<Self>>> {
+        enum FoldIndicators {}
+
+        let style = style.folds.clone();
+
+        fold_data
+            .iter()
+            .enumerate()
+            .map(|(ix, fold_data)| {
+                fold_data
+                    .map(|(fold_status, buffer_row, active)| {
+                        (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
+                            MouseEventHandler::new::<FoldIndicators, _>(
+                                ix as usize,
+                                cx,
+                                |mouse_state, _| {
+                                    Svg::new(match fold_status {
+                                        FoldStatus::Folded => style.folded_icon.clone(),
+                                        FoldStatus::Foldable => style.foldable_icon.clone(),
+                                    })
+                                    .with_color(
+                                        style
+                                            .indicator
+                                            .in_state(fold_status == FoldStatus::Folded)
+                                            .style_for(mouse_state)
+                                            .color,
+                                    )
+                                    .constrained()
+                                    .with_width(gutter_margin * style.icon_margin_scale)
+                                    .aligned()
+                                    .constrained()
+                                    .with_height(line_height)
+                                    .with_width(gutter_margin)
+                                    .aligned()
+                                },
+                            )
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .with_padding(Padding::uniform(3.))
+                            .on_click(MouseButton::Left, {
+                                move |_, editor, cx| match fold_status {
+                                    FoldStatus::Folded => {
+                                        editor.unfold_at(&UnfoldAt { buffer_row }, cx);
+                                    }
+                                    FoldStatus::Foldable => {
+                                        editor.fold_at(&FoldAt { buffer_row }, cx);
+                                    }
+                                }
+                            })
+                            .into_any()
+                        })
+                    })
+                    .flatten()
+            })
+            .collect()
+    }
+
+    pub fn context_menu_visible(&self) -> bool {
+        self.context_menu
+            .read()
+            .as_ref()
+            .map_or(false, |menu| menu.visible())
+    }
+
+    pub fn render_context_menu(
+        &self,
+        cursor_position: DisplayPoint,
+        style: EditorStyle,
+        cx: &mut ViewContext<Editor>,
+    ) -> Option<(DisplayPoint, AnyElement<Editor>)> {
+        self.context_menu.read().as_ref().map(|menu| {
+            menu.render(
+                cursor_position,
+                style,
+                self.workspace.as_ref().map(|(w, _)| w.clone()),
+                cx,
+            )
+        })
+    }
+
+    fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<ContextMenu> {
+        cx.notify();
+        self.completion_tasks.clear();
+        let context_menu = self.context_menu.write().take();
+        if context_menu.is_some() {
+            self.update_visible_copilot_suggestion(cx);
+        }
+        context_menu
+    }
+
+    pub fn insert_snippet(
+        &mut self,
+        insertion_ranges: &[Range<usize>],
+        snippet: Snippet,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()> {
+        let tabstops = self.buffer.update(cx, |buffer, cx| {
+            let snippet_text: Arc<str> = snippet.text.clone().into();
+            buffer.edit(
+                insertion_ranges
+                    .iter()
+                    .cloned()
+                    .map(|range| (range, snippet_text.clone())),
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+
+            let snapshot = &*buffer.read(cx);
+            let snippet = &snippet;
+            snippet
+                .tabstops
+                .iter()
+                .map(|tabstop| {
+                    let mut tabstop_ranges = tabstop
+                        .iter()
+                        .flat_map(|tabstop_range| {
+                            let mut delta = 0_isize;
+                            insertion_ranges.iter().map(move |insertion_range| {
+                                let insertion_start = insertion_range.start as isize + delta;
+                                delta +=
+                                    snippet.text.len() as isize - insertion_range.len() as isize;
+
+                                let start = snapshot.anchor_before(
+                                    (insertion_start + tabstop_range.start) as usize,
+                                );
+                                let end = snapshot
+                                    .anchor_after((insertion_start + tabstop_range.end) as usize);
+                                start..end
+                            })
+                        })
+                        .collect::<Vec<_>>();
+                    tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot));
+                    tabstop_ranges
+                })
+                .collect::<Vec<_>>()
+        });
+
+        if let Some(tabstop) = tabstops.first() {
+            self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select_ranges(tabstop.iter().cloned());
+            });
+            self.snippet_stack.push(SnippetState {
+                active_index: 0,
+                ranges: tabstops,
+            });
+        }
+
+        Ok(())
+    }
+
+    pub fn move_to_next_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        self.move_to_snippet_tabstop(Bias::Right, cx)
+    }
+
+    pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        self.move_to_snippet_tabstop(Bias::Left, cx)
+    }
+
+    pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext<Self>) -> bool {
+        if let Some(mut snippet) = self.snippet_stack.pop() {
+            match bias {
+                Bias::Left => {
+                    if snippet.active_index > 0 {
+                        snippet.active_index -= 1;
+                    } else {
+                        self.snippet_stack.push(snippet);
+                        return false;
+                    }
+                }
+                Bias::Right => {
+                    if snippet.active_index + 1 < snippet.ranges.len() {
+                        snippet.active_index += 1;
+                    } else {
+                        self.snippet_stack.push(snippet);
+                        return false;
+                    }
+                }
+            }
+            if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) {
+                self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.select_anchor_ranges(current_ranges.iter().cloned())
+                });
+                // If snippet state is not at the last tabstop, push it back on the stack
+                if snippet.active_index + 1 < snippet.ranges.len() {
+                    self.snippet_stack.push(snippet);
+                }
+                return true;
+            }
+        }
+
+        false
+    }
+
+    pub fn clear(&mut self, cx: &mut ViewContext<Self>) {
+        self.transact(cx, |this, cx| {
+            this.select_all(&SelectAll, cx);
+            this.insert("", cx);
+        });
+    }
+
+    pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
+        self.transact(cx, |this, cx| {
+            this.select_autoclose_pair(cx);
+            let mut selections = this.selections.all::<Point>(cx);
+            if !this.selections.line_mode {
+                let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
+                for selection in &mut selections {
+                    if selection.is_empty() {
+                        let old_head = selection.head();
+                        let mut new_head =
+                            movement::left(&display_map, old_head.to_display_point(&display_map))
+                                .to_point(&display_map);
+                        if let Some((buffer, line_buffer_range)) = display_map
+                            .buffer_snapshot
+                            .buffer_line_for_row(old_head.row)
+                        {
+                            let indent_size =
+                                buffer.indent_size_for_line(line_buffer_range.start.row);
+                            let indent_len = match indent_size.kind {
+                                IndentKind::Space => {
+                                    buffer.settings_at(line_buffer_range.start, cx).tab_size
+                                }
+                                IndentKind::Tab => NonZeroU32::new(1).unwrap(),
+                            };
+                            if old_head.column <= indent_size.len && old_head.column > 0 {
+                                let indent_len = indent_len.get();
+                                new_head = cmp::min(
+                                    new_head,
+                                    Point::new(
+                                        old_head.row,
+                                        ((old_head.column - 1) / indent_len) * indent_len,
+                                    ),
+                                );
+                            }
+                        }
+
+                        selection.set_head(new_head, SelectionGoal::None);
+                    }
+                }
+            }
+
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+            this.insert("", cx);
+            this.refresh_copilot_suggestions(true, cx);
+        });
+    }
+
+    pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
+        self.transact(cx, |this, cx| {
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                let line_mode = s.line_mode;
+                s.move_with(|map, selection| {
+                    if selection.is_empty() && !line_mode {
+                        let cursor = movement::right(map, selection.head());
+                        selection.end = cursor;
+                        selection.reversed = true;
+                        selection.goal = SelectionGoal::None;
+                    }
+                })
+            });
+            this.insert("", cx);
+            this.refresh_copilot_suggestions(true, cx);
+        });
+    }
+
+    pub fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
+        if self.move_to_prev_snippet_tabstop(cx) {
+            return;
+        }
+
+        self.outdent(&Outdent, cx);
+    }
+
+    pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+        if self.move_to_next_snippet_tabstop(cx) {
+            return;
+        }
+
+        let mut selections = self.selections.all_adjusted(cx);
+        let buffer = self.buffer.read(cx);
+        let snapshot = buffer.snapshot(cx);
+        let rows_iter = selections.iter().map(|s| s.head().row);
+        let suggested_indents = snapshot.suggested_indents(rows_iter, cx);
+
+        let mut edits = Vec::new();
+        let mut prev_edited_row = 0;
+        let mut row_delta = 0;
+        for selection in &mut selections {
+            if selection.start.row != prev_edited_row {
+                row_delta = 0;
+            }
+            prev_edited_row = selection.end.row;
+
+            // If the selection is non-empty, then increase the indentation of the selected lines.
+            if !selection.is_empty() {
+                row_delta =
+                    Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx);
+                continue;
+            }
+
+            // If the selection is empty and the cursor is in the leading whitespace before the
+            // suggested indentation, then auto-indent the line.
+            let cursor = selection.head();
+            let current_indent = snapshot.indent_size_for_line(cursor.row);
+            if let Some(suggested_indent) = suggested_indents.get(&cursor.row).copied() {
+                if cursor.column < suggested_indent.len
+                    && cursor.column <= current_indent.len
+                    && current_indent.len <= suggested_indent.len
+                {
+                    selection.start = Point::new(cursor.row, suggested_indent.len);
+                    selection.end = selection.start;
+                    if row_delta == 0 {
+                        edits.extend(Buffer::edit_for_indent_size_adjustment(
+                            cursor.row,
+                            current_indent,
+                            suggested_indent,
+                        ));
+                        row_delta = suggested_indent.len - current_indent.len;
+                    }
+                    continue;
+                }
+            }
+
+            // Accept copilot suggestion if there is only one selection and the cursor is not
+            // in the leading whitespace.
+            if self.selections.count() == 1
+                && cursor.column >= current_indent.len
+                && self.has_active_copilot_suggestion(cx)
+            {
+                self.accept_copilot_suggestion(cx);
+                return;
+            }
+
+            // Otherwise, insert a hard or soft tab.
+            let settings = buffer.settings_at(cursor, cx);
+            let tab_size = if settings.hard_tabs {
+                IndentSize::tab()
+            } else {
+                let tab_size = settings.tab_size.get();
+                let char_column = snapshot
+                    .text_for_range(Point::new(cursor.row, 0)..cursor)
+                    .flat_map(str::chars)
+                    .count()
+                    + row_delta as usize;
+                let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size);
+                IndentSize::spaces(chars_to_next_tab_stop)
+            };
+            selection.start = Point::new(cursor.row, cursor.column + row_delta + tab_size.len);
+            selection.end = selection.start;
+            edits.push((cursor..cursor, tab_size.chars().collect::<String>()));
+            row_delta += tab_size.len;
+        }
+
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+            this.refresh_copilot_suggestions(true, cx);
+        });
+    }
+
+    pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext<Self>) {
+        let mut selections = self.selections.all::<Point>(cx);
+        let mut prev_edited_row = 0;
+        let mut row_delta = 0;
+        let mut edits = Vec::new();
+        let buffer = self.buffer.read(cx);
+        let snapshot = buffer.snapshot(cx);
+        for selection in &mut selections {
+            if selection.start.row != prev_edited_row {
+                row_delta = 0;
+            }
+            prev_edited_row = selection.end.row;
+
+            row_delta =
+                Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx);
+        }
+
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+        });
+    }
+
+    fn indent_selection(
+        buffer: &MultiBuffer,
+        snapshot: &MultiBufferSnapshot,
+        selection: &mut Selection<Point>,
+        edits: &mut Vec<(Range<Point>, String)>,
+        delta_for_start_row: u32,
+        cx: &AppContext,
+    ) -> u32 {
+        let settings = buffer.settings_at(selection.start, cx);
+        let tab_size = settings.tab_size.get();
+        let indent_kind = if settings.hard_tabs {
+            IndentKind::Tab
+        } else {
+            IndentKind::Space
+        };
+        let mut start_row = selection.start.row;
+        let mut end_row = selection.end.row + 1;
+
+        // If a selection ends at the beginning of a line, don't indent
+        // that last line.
+        if selection.end.column == 0 {
+            end_row -= 1;
+        }
+
+        // Avoid re-indenting a row that has already been indented by a
+        // previous selection, but still update this selection's column
+        // to reflect that indentation.
+        if delta_for_start_row > 0 {
+            start_row += 1;
+            selection.start.column += delta_for_start_row;
+            if selection.end.row == selection.start.row {
+                selection.end.column += delta_for_start_row;
+            }
+        }
+
+        let mut delta_for_end_row = 0;
+        for row in start_row..end_row {
+            let current_indent = snapshot.indent_size_for_line(row);
+            let indent_delta = match (current_indent.kind, indent_kind) {
+                (IndentKind::Space, IndentKind::Space) => {
+                    let columns_to_next_tab_stop = tab_size - (current_indent.len % tab_size);
+                    IndentSize::spaces(columns_to_next_tab_stop)
+                }
+                (IndentKind::Tab, IndentKind::Space) => IndentSize::spaces(tab_size),
+                (_, IndentKind::Tab) => IndentSize::tab(),
+            };
+
+            let row_start = Point::new(row, 0);
+            edits.push((
+                row_start..row_start,
+                indent_delta.chars().collect::<String>(),
+            ));
+
+            // Update this selection's endpoints to reflect the indentation.
+            if row == selection.start.row {
+                selection.start.column += indent_delta.len;
+            }
+            if row == selection.end.row {
+                selection.end.column += indent_delta.len;
+                delta_for_end_row = indent_delta.len;
+            }
+        }
+
+        if selection.start.row == selection.end.row {
+            delta_for_start_row + delta_for_end_row
+        } else {
+            delta_for_end_row
+        }
+    }
+
+    pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let selections = self.selections.all::<Point>(cx);
+        let mut deletion_ranges = Vec::new();
+        let mut last_outdent = None;
+        {
+            let buffer = self.buffer.read(cx);
+            let snapshot = buffer.snapshot(cx);
+            for selection in &selections {
+                let settings = buffer.settings_at(selection.start, cx);
+                let tab_size = settings.tab_size.get();
+                let mut rows = selection.spanned_rows(false, &display_map);
+
+                // Avoid re-outdenting a row that has already been outdented by a
+                // previous selection.
+                if let Some(last_row) = last_outdent {
+                    if last_row == rows.start {
+                        rows.start += 1;
+                    }
+                }
+
+                for row in rows {
+                    let indent_size = snapshot.indent_size_for_line(row);
+                    if indent_size.len > 0 {
+                        let deletion_len = match indent_size.kind {
+                            IndentKind::Space => {
+                                let columns_to_prev_tab_stop = indent_size.len % tab_size;
+                                if columns_to_prev_tab_stop == 0 {
+                                    tab_size
+                                } else {
+                                    columns_to_prev_tab_stop
+                                }
+                            }
+                            IndentKind::Tab => 1,
+                        };
+                        deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len));
+                        last_outdent = Some(row);
+                    }
+                }
+            }
+        }
+
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                let empty_str: Arc<str> = "".into();
+                buffer.edit(
+                    deletion_ranges
+                        .into_iter()
+                        .map(|range| (range, empty_str.clone())),
+                    None,
+                    cx,
+                );
+            });
+            let selections = this.selections.all::<usize>(cx);
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+        });
+    }
+
+    pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let selections = self.selections.all::<Point>(cx);
+
+        let mut new_cursors = Vec::new();
+        let mut edit_ranges = Vec::new();
+        let mut selections = selections.iter().peekable();
+        while let Some(selection) = selections.next() {
+            let mut rows = selection.spanned_rows(false, &display_map);
+            let goal_display_column = selection.head().to_display_point(&display_map).column();
+
+            // Accumulate contiguous regions of rows that we want to delete.
+            while let Some(next_selection) = selections.peek() {
+                let next_rows = next_selection.spanned_rows(false, &display_map);
+                if next_rows.start <= rows.end {
+                    rows.end = next_rows.end;
+                    selections.next().unwrap();
+                } else {
+                    break;
+                }
+            }
+
+            let buffer = &display_map.buffer_snapshot;
+            let mut edit_start = Point::new(rows.start, 0).to_offset(buffer);
+            let edit_end;
+            let cursor_buffer_row;
+            if buffer.max_point().row >= rows.end {
+                // If there's a line after the range, delete the \n from the end of the row range
+                // and position the cursor on the next line.
+                edit_end = Point::new(rows.end, 0).to_offset(buffer);
+                cursor_buffer_row = rows.end;
+            } else {
+                // If there isn't a line after the range, delete the \n from the line before the
+                // start of the row range and position the cursor there.
+                edit_start = edit_start.saturating_sub(1);
+                edit_end = buffer.len();
+                cursor_buffer_row = rows.start.saturating_sub(1);
+            }
+
+            let mut cursor = Point::new(cursor_buffer_row, 0).to_display_point(&display_map);
+            *cursor.column_mut() =
+                cmp::min(goal_display_column, display_map.line_len(cursor.row()));
+
+            new_cursors.push((
+                selection.id,
+                buffer.anchor_after(cursor.to_point(&display_map)),
+            ));
+            edit_ranges.push(edit_start..edit_end);
+        }
+
+        self.transact(cx, |this, cx| {
+            let buffer = this.buffer.update(cx, |buffer, cx| {
+                let empty_str: Arc<str> = "".into();
+                buffer.edit(
+                    edit_ranges
+                        .into_iter()
+                        .map(|range| (range, empty_str.clone())),
+                    None,
+                    cx,
+                );
+                buffer.snapshot(cx)
+            });
+            let new_selections = new_cursors
+                .into_iter()
+                .map(|(id, cursor)| {
+                    let cursor = cursor.to_point(&buffer);
+                    Selection {
+                        id,
+                        start: cursor,
+                        end: cursor,
+                        reversed: false,
+                        goal: SelectionGoal::None,
+                    }
+                })
+                .collect();
+
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(new_selections);
+            });
+        });
+    }
+
+    pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
+        let mut row_ranges = Vec::<Range<u32>>::new();
+        for selection in self.selections.all::<Point>(cx) {
+            let start = selection.start.row;
+            let end = if selection.start.row == selection.end.row {
+                selection.start.row + 1
+            } else {
+                selection.end.row
+            };
+
+            if let Some(last_row_range) = row_ranges.last_mut() {
+                if start <= last_row_range.end {
+                    last_row_range.end = end;
+                    continue;
+                }
+            }
+            row_ranges.push(start..end);
+        }
+
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let mut cursor_positions = Vec::new();
+        for row_range in &row_ranges {
+            let anchor = snapshot.anchor_before(Point::new(
+                row_range.end - 1,
+                snapshot.line_len(row_range.end - 1),
+            ));
+            cursor_positions.push(anchor.clone()..anchor);
+        }
+
+        self.transact(cx, |this, cx| {
+            for row_range in row_ranges.into_iter().rev() {
+                for row in row_range.rev() {
+                    let end_of_line = Point::new(row, snapshot.line_len(row));
+                    let indent = snapshot.indent_size_for_line(row + 1);
+                    let start_of_next_line = Point::new(row + 1, indent.len);
+
+                    let replace = if snapshot.line_len(row + 1) > indent.len {
+                        " "
+                    } else {
+                        ""
+                    };
+
+                    this.buffer.update(cx, |buffer, cx| {
+                        buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
+                    });
+                }
+            }
+
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select_anchor_ranges(cursor_positions)
+            });
+        });
+    }
+
+    pub fn sort_lines_case_sensitive(
+        &mut self,
+        _: &SortLinesCaseSensitive,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_lines(cx, |lines| lines.sort())
+    }
+
+    pub fn sort_lines_case_insensitive(
+        &mut self,
+        _: &SortLinesCaseInsensitive,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase()))
+    }
+
+    pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
+        self.manipulate_lines(cx, |lines| lines.reverse())
+    }
+
+    pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext<Self>) {
+        self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng()))
+    }
+
+    fn manipulate_lines<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
+    where
+        Fn: FnMut(&mut [&str]),
+    {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = self.buffer.read(cx).snapshot(cx);
+
+        let mut edits = Vec::new();
+
+        let selections = self.selections.all::<Point>(cx);
+        let mut selections = selections.iter().peekable();
+        let mut contiguous_row_selections = Vec::new();
+        let mut new_selections = Vec::new();
+
+        while let Some(selection) = selections.next() {
+            let (start_row, end_row) = consume_contiguous_rows(
+                &mut contiguous_row_selections,
+                selection,
+                &display_map,
+                &mut selections,
+            );
+
+            let start_point = Point::new(start_row, 0);
+            let end_point = Point::new(end_row - 1, buffer.line_len(end_row - 1));
+            let text = buffer
+                .text_for_range(start_point..end_point)
+                .collect::<String>();
+            let mut lines = text.split("\n").collect_vec();
+
+            let lines_len = lines.len();
+            callback(&mut lines);
+
+            // This is a current limitation with selections.
+            // If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections.
+            debug_assert!(
+                lines.len() == lines_len,
+                "callback should not change the number of lines"
+            );
+
+            edits.push((start_point..end_point, lines.join("\n")));
+            let start_anchor = buffer.anchor_after(start_point);
+            let end_anchor = buffer.anchor_before(end_point);
+
+            // Make selection and push
+            new_selections.push(Selection {
+                id: selection.id,
+                start: start_anchor.to_offset(&buffer),
+                end: end_anchor.to_offset(&buffer),
+                goal: SelectionGoal::None,
+                reversed: selection.reversed,
+            });
+        }
+
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, None, cx);
+            });
+
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(new_selections);
+            });
+
+            this.request_autoscroll(Autoscroll::fit(), cx);
+        });
+    }
+
+    pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_uppercase())
+    }
+
+    pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_lowercase())
+    }
+
+    pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| {
+            // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
+            // https://github.com/rutrum/convert-case/issues/16
+            text.split("\n")
+                .map(|line| line.to_case(Case::Title))
+                .join("\n")
+        })
+    }
+
+    pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Snake))
+    }
+
+    pub fn convert_to_kebab_case(&mut self, _: &ConvertToKebabCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Kebab))
+    }
+
+    pub fn convert_to_upper_camel_case(
+        &mut self,
+        _: &ConvertToUpperCamelCase,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_text(cx, |text| {
+            // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
+            // https://github.com/rutrum/convert-case/issues/16
+            text.split("\n")
+                .map(|line| line.to_case(Case::UpperCamel))
+                .join("\n")
+        })
+    }
+
+    pub fn convert_to_lower_camel_case(
+        &mut self,
+        _: &ConvertToLowerCamelCase,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Camel))
+    }
+
+    fn manipulate_text<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
+    where
+        Fn: FnMut(&str) -> String,
+    {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = self.buffer.read(cx).snapshot(cx);
+
+        let mut new_selections = Vec::new();
+        let mut edits = Vec::new();
+        let mut selection_adjustment = 0i32;
+
+        for selection in self.selections.all::<usize>(cx) {
+            let selection_is_empty = selection.is_empty();
+
+            let (start, end) = if selection_is_empty {
+                let word_range = movement::surrounding_word(
+                    &display_map,
+                    selection.start.to_display_point(&display_map),
+                );
+                let start = word_range.start.to_offset(&display_map, Bias::Left);
+                let end = word_range.end.to_offset(&display_map, Bias::Left);
+                (start, end)
+            } else {
+                (selection.start, selection.end)
+            };
+
+            let text = buffer.text_for_range(start..end).collect::<String>();
+            let old_length = text.len() as i32;
+            let text = callback(&text);
+
+            new_selections.push(Selection {
+                start: (start as i32 - selection_adjustment) as usize,
+                end: ((start + text.len()) as i32 - selection_adjustment) as usize,
+                goal: SelectionGoal::None,
+                ..selection
+            });
+
+            selection_adjustment += old_length - text.len() as i32;
+
+            edits.push((start..end, text));
+        }
+
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, None, cx);
+            });
+
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(new_selections);
+            });
+
+            this.request_autoscroll(Autoscroll::fit(), cx);
+        });
+    }
+
+    pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = &display_map.buffer_snapshot;
+        let selections = self.selections.all::<Point>(cx);
+
+        let mut edits = Vec::new();
+        let mut selections_iter = selections.iter().peekable();
+        while let Some(selection) = selections_iter.next() {
+            // Avoid duplicating the same lines twice.
+            let mut rows = selection.spanned_rows(false, &display_map);
+
+            while let Some(next_selection) = selections_iter.peek() {
+                let next_rows = next_selection.spanned_rows(false, &display_map);
+                if next_rows.start < rows.end {
+                    rows.end = next_rows.end;
+                    selections_iter.next().unwrap();
+                } else {
+                    break;
+                }
+            }
+
+            // Copy the text from the selected row region and splice it at the start of the region.
+            let start = Point::new(rows.start, 0);
+            let end = Point::new(rows.end - 1, buffer.line_len(rows.end - 1));
+            let text = buffer
+                .text_for_range(start..end)
+                .chain(Some("\n"))
+                .collect::<String>();
+            edits.push((start..start, text));
+        }
+
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, None, cx);
+            });
+
+            this.request_autoscroll(Autoscroll::fit(), cx);
+        });
+    }
+
+    pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = self.buffer.read(cx).snapshot(cx);
+
+        let mut edits = Vec::new();
+        let mut unfold_ranges = Vec::new();
+        let mut refold_ranges = Vec::new();
+
+        let selections = self.selections.all::<Point>(cx);
+        let mut selections = selections.iter().peekable();
+        let mut contiguous_row_selections = Vec::new();
+        let mut new_selections = Vec::new();
+
+        while let Some(selection) = selections.next() {
+            // Find all the selections that span a contiguous row range
+            let (start_row, end_row) = consume_contiguous_rows(
+                &mut contiguous_row_selections,
+                selection,
+                &display_map,
+                &mut selections,
+            );
+
+            // Move the text spanned by the row range to be before the line preceding the row range
+            if start_row > 0 {
+                let range_to_move = Point::new(start_row - 1, buffer.line_len(start_row - 1))
+                    ..Point::new(end_row - 1, buffer.line_len(end_row - 1));
+                let insertion_point = display_map
+                    .prev_line_boundary(Point::new(start_row - 1, 0))
+                    .0;
+
+                // Don't move lines across excerpts
+                if buffer
+                    .excerpt_boundaries_in_range((
+                        Bound::Excluded(insertion_point),
+                        Bound::Included(range_to_move.end),
+                    ))
+                    .next()
+                    .is_none()
+                {
+                    let text = buffer
+                        .text_for_range(range_to_move.clone())
+                        .flat_map(|s| s.chars())
+                        .skip(1)
+                        .chain(['\n'])
+                        .collect::<String>();
+
+                    edits.push((
+                        buffer.anchor_after(range_to_move.start)
+                            ..buffer.anchor_before(range_to_move.end),
+                        String::new(),
+                    ));
+                    let insertion_anchor = buffer.anchor_after(insertion_point);
+                    edits.push((insertion_anchor..insertion_anchor, text));
+
+                    let row_delta = range_to_move.start.row - insertion_point.row + 1;
+
+                    // Move selections up
+                    new_selections.extend(contiguous_row_selections.drain(..).map(
+                        |mut selection| {
+                            selection.start.row -= row_delta;
+                            selection.end.row -= row_delta;
+                            selection
+                        },
+                    ));
+
+                    // Move folds up
+                    unfold_ranges.push(range_to_move.clone());
+                    for fold in display_map.folds_in_range(
+                        buffer.anchor_before(range_to_move.start)
+                            ..buffer.anchor_after(range_to_move.end),
+                    ) {
+                        let mut start = fold.start.to_point(&buffer);
+                        let mut end = fold.end.to_point(&buffer);
+                        start.row -= row_delta;
+                        end.row -= row_delta;
+                        refold_ranges.push(start..end);
+                    }
+                }
+            }
+
+            // If we didn't move line(s), preserve the existing selections
+            new_selections.append(&mut contiguous_row_selections);
+        }
+
+        self.transact(cx, |this, cx| {
+            this.unfold_ranges(unfold_ranges, true, true, cx);
+            this.buffer.update(cx, |buffer, cx| {
+                for (range, text) in edits {
+                    buffer.edit([(range, text)], None, cx);
+                }
+            });
+            this.fold_ranges(refold_ranges, true, cx);
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(new_selections);
+            })
+        });
+    }
+
+    pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = self.buffer.read(cx).snapshot(cx);
+
+        let mut edits = Vec::new();
+        let mut unfold_ranges = Vec::new();
+        let mut refold_ranges = Vec::new();
+
+        let selections = self.selections.all::<Point>(cx);
+        let mut selections = selections.iter().peekable();
+        let mut contiguous_row_selections = Vec::new();
+        let mut new_selections = Vec::new();
+
+        while let Some(selection) = selections.next() {
+            // Find all the selections that span a contiguous row range
+            let (start_row, end_row) = consume_contiguous_rows(
+                &mut contiguous_row_selections,
+                selection,
+                &display_map,
+                &mut selections,
+            );
+
+            // Move the text spanned by the row range to be after the last line of the row range
+            if end_row <= buffer.max_point().row {
+                let range_to_move = Point::new(start_row, 0)..Point::new(end_row, 0);
+                let insertion_point = display_map.next_line_boundary(Point::new(end_row, 0)).0;
+
+                // Don't move lines across excerpt boundaries
+                if buffer
+                    .excerpt_boundaries_in_range((
+                        Bound::Excluded(range_to_move.start),
+                        Bound::Included(insertion_point),
+                    ))
+                    .next()
+                    .is_none()
+                {
+                    let mut text = String::from("\n");
+                    text.extend(buffer.text_for_range(range_to_move.clone()));
+                    text.pop(); // Drop trailing newline
+                    edits.push((
+                        buffer.anchor_after(range_to_move.start)
+                            ..buffer.anchor_before(range_to_move.end),
+                        String::new(),
+                    ));
+                    let insertion_anchor = buffer.anchor_after(insertion_point);
+                    edits.push((insertion_anchor..insertion_anchor, text));
+
+                    let row_delta = insertion_point.row - range_to_move.end.row + 1;
+
+                    // Move selections down
+                    new_selections.extend(contiguous_row_selections.drain(..).map(
+                        |mut selection| {
+                            selection.start.row += row_delta;
+                            selection.end.row += row_delta;
+                            selection
+                        },
+                    ));
+
+                    // Move folds down
+                    unfold_ranges.push(range_to_move.clone());
+                    for fold in display_map.folds_in_range(
+                        buffer.anchor_before(range_to_move.start)
+                            ..buffer.anchor_after(range_to_move.end),
+                    ) {
+                        let mut start = fold.start.to_point(&buffer);
+                        let mut end = fold.end.to_point(&buffer);
+                        start.row += row_delta;
+                        end.row += row_delta;
+                        refold_ranges.push(start..end);
+                    }
+                }
+            }
+
+            // If we didn't move line(s), preserve the existing selections
+            new_selections.append(&mut contiguous_row_selections);
+        }
+
+        self.transact(cx, |this, cx| {
+            this.unfold_ranges(unfold_ranges, true, true, cx);
+            this.buffer.update(cx, |buffer, cx| {
+                for (range, text) in edits {
+                    buffer.edit([(range, text)], None, cx);
+                }
+            });
+            this.fold_ranges(refold_ranges, true, cx);
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
+        });
+    }
+
+    pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
+        self.transact(cx, |this, cx| {
+            let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                let mut edits: Vec<(Range<usize>, String)> = Default::default();
+                let line_mode = s.line_mode;
+                s.move_with(|display_map, selection| {
+                    if !selection.is_empty() || line_mode {
+                        return;
+                    }
+
+                    let mut head = selection.head();
+                    let mut transpose_offset = head.to_offset(display_map, Bias::Right);
+                    if head.column() == display_map.line_len(head.row()) {
+                        transpose_offset = display_map
+                            .buffer_snapshot
+                            .clip_offset(transpose_offset.saturating_sub(1), Bias::Left);
+                    }
+
+                    if transpose_offset == 0 {
+                        return;
+                    }
+
+                    *head.column_mut() += 1;
+                    head = display_map.clip_point(head, Bias::Right);
+                    let goal = SelectionGoal::HorizontalPosition(
+                        display_map.x_for_point(head, &text_layout_details),
+                    );
+                    selection.collapse_to(head, goal);
+
+                    let transpose_start = display_map
+                        .buffer_snapshot
+                        .clip_offset(transpose_offset.saturating_sub(1), Bias::Left);
+                    if edits.last().map_or(true, |e| e.0.end <= transpose_start) {
+                        let transpose_end = display_map
+                            .buffer_snapshot
+                            .clip_offset(transpose_offset + 1, Bias::Right);
+                        if let Some(ch) =
+                            display_map.buffer_snapshot.chars_at(transpose_start).next()
+                        {
+                            edits.push((transpose_start..transpose_offset, String::new()));
+                            edits.push((transpose_end..transpose_end, ch.to_string()));
+                        }
+                    }
+                });
+                edits
+            });
+            this.buffer
+                .update(cx, |buffer, cx| buffer.edit(edits, None, cx));
+            let selections = this.selections.all::<usize>(cx);
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(selections);
+            });
+        });
+    }
+
+    pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
+        let mut text = String::new();
+        let buffer = self.buffer.read(cx).snapshot(cx);
+        let mut selections = self.selections.all::<Point>(cx);
+        let mut clipboard_selections = Vec::with_capacity(selections.len());
+        {
+            let max_point = buffer.max_point();
+            let mut is_first = true;
+            for selection in &mut selections {
+                let is_entire_line = selection.is_empty() || self.selections.line_mode;
+                if is_entire_line {
+                    selection.start = Point::new(selection.start.row, 0);
+                    selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
+                    selection.goal = SelectionGoal::None;
+                }
+                if is_first {
+                    is_first = false;
+                } else {
+                    text += "\n";
+                }
+                let mut len = 0;
+                for chunk in buffer.text_for_range(selection.start..selection.end) {
+                    text.push_str(chunk);
+                    len += chunk.len();
+                }
+                clipboard_selections.push(ClipboardSelection {
+                    len,
+                    is_entire_line,
+                    first_line_indent: buffer.indent_size_for_line(selection.start.row).len,
+                });
+            }
+        }
+
+        self.transact(cx, |this, cx| {
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(selections);
+            });
+            this.insert("", cx);
+            cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
+        });
+    }
+
+    pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
+        let selections = self.selections.all::<Point>(cx);
+        let buffer = self.buffer.read(cx).read(cx);
+        let mut text = String::new();
+
+        let mut clipboard_selections = Vec::with_capacity(selections.len());
+        {
+            let max_point = buffer.max_point();
+            let mut is_first = true;
+            for selection in selections.iter() {
+                let mut start = selection.start;
+                let mut end = selection.end;
+                let is_entire_line = selection.is_empty() || self.selections.line_mode;
+                if is_entire_line {
+                    start = Point::new(start.row, 0);
+                    end = cmp::min(max_point, Point::new(end.row + 1, 0));
+                }
+                if is_first {
+                    is_first = false;
+                } else {
+                    text += "\n";
+                }
+                let mut len = 0;
+                for chunk in buffer.text_for_range(start..end) {
+                    text.push_str(chunk);
+                    len += chunk.len();
+                }
+                clipboard_selections.push(ClipboardSelection {
+                    len,
+                    is_entire_line,
+                    first_line_indent: buffer.indent_size_for_line(start.row).len,
+                });
+            }
+        }
+
+        cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
+    }
+
+    pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+        self.transact(cx, |this, cx| {
+            if let Some(item) = cx.read_from_clipboard() {
+                let clipboard_text = Cow::Borrowed(item.text());
+                if let Some(mut clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
+                    let old_selections = this.selections.all::<usize>(cx);
+                    let all_selections_were_entire_line =
+                        clipboard_selections.iter().all(|s| s.is_entire_line);
+                    let first_selection_indent_column =
+                        clipboard_selections.first().map(|s| s.first_line_indent);
+                    if clipboard_selections.len() != old_selections.len() {
+                        clipboard_selections.drain(..);
+                    }
+
+                    this.buffer.update(cx, |buffer, cx| {
+                        let snapshot = buffer.read(cx);
+                        let mut start_offset = 0;
+                        let mut edits = Vec::new();
+                        let mut original_indent_columns = Vec::new();
+                        let line_mode = this.selections.line_mode;
+                        for (ix, selection) in old_selections.iter().enumerate() {
+                            let to_insert;
+                            let entire_line;
+                            let original_indent_column;
+                            if let Some(clipboard_selection) = clipboard_selections.get(ix) {
+                                let end_offset = start_offset + clipboard_selection.len;
+                                to_insert = &clipboard_text[start_offset..end_offset];
+                                entire_line = clipboard_selection.is_entire_line;
+                                start_offset = end_offset + 1;
+                                original_indent_column =
+                                    Some(clipboard_selection.first_line_indent);
+                            } else {
+                                to_insert = clipboard_text.as_str();
+                                entire_line = all_selections_were_entire_line;
+                                original_indent_column = first_selection_indent_column
+                            }
+
+                            // If the corresponding selection was empty when this slice of the
+                            // clipboard text was written, then the entire line containing the
+                            // selection was copied. If this selection is also currently empty,
+                            // then paste the line before the current line of the buffer.
+                            let range = if selection.is_empty() && !line_mode && entire_line {
+                                let column = selection.start.to_point(&snapshot).column as usize;
+                                let line_start = selection.start - column;
+                                line_start..line_start
+                            } else {
+                                selection.range()
+                            };
+
+                            edits.push((range, to_insert));
+                            original_indent_columns.extend(original_indent_column);
+                        }
+                        drop(snapshot);
+
+                        buffer.edit(
+                            edits,
+                            Some(AutoindentMode::Block {
+                                original_indent_columns,
+                            }),
+                            cx,
+                        );
+                    });
+
+                    let selections = this.selections.all::<usize>(cx);
+                    this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+                } else {
+                    this.insert(&clipboard_text, cx);
+                }
+            }
+        });
+    }
+
+    pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
+        if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
+            if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() {
+                self.change_selections(None, cx, |s| {
+                    s.select_anchors(selections.to_vec());
+                });
+            }
+            self.request_autoscroll(Autoscroll::fit(), cx);
+            self.unmark_text(cx);
+            self.refresh_copilot_suggestions(true, cx);
+            cx.emit(Event::Edited);
+        }
+    }
+
+    pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
+        if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
+            if let Some((_, Some(selections))) = self.selection_history.transaction(tx_id).cloned()
+            {
+                self.change_selections(None, cx, |s| {
+                    s.select_anchors(selections.to_vec());
+                });
+            }
+            self.request_autoscroll(Autoscroll::fit(), cx);
+            self.unmark_text(cx);
+            self.refresh_copilot_suggestions(true, cx);
+            cx.emit(Event::Edited);
+        }
+    }
+
+    pub fn finalize_last_transaction(&mut self, cx: &mut ViewContext<Self>) {
+        self.buffer
+            .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx));
+    }
+
+    pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            let line_mode = s.line_mode;
+            s.move_with(|map, selection| {
+                let cursor = if selection.is_empty() && !line_mode {
+                    movement::left(map, selection.start)
+                } else {
+                    selection.start
+                };
+                selection.collapse_to(cursor, SelectionGoal::None);
+            });
+        })
+    }
+
+    pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None));
+        })
+    }
+
+    pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext<Self>) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            let line_mode = s.line_mode;
+            s.move_with(|map, selection| {
+                let cursor = if selection.is_empty() && !line_mode {
+                    movement::right(map, selection.end)
+                } else {
+                    selection.end
+                };
+                selection.collapse_to(cursor, SelectionGoal::None)
+            });
+        })
+    }
+
+    pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None));
+        })
+    }
+
+    pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
+        if self.take_rename(true, cx).is_some() {
+            return;
+        }
+
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        let text_layout_details = &self.text_layout_details(cx);
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            let line_mode = s.line_mode;
+            s.move_with(|map, selection| {
+                if !selection.is_empty() && !line_mode {
+                    selection.goal = SelectionGoal::None;
+                }
+                let (cursor, goal) = movement::up(
+                    map,
+                    selection.start,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
+                selection.collapse_to(cursor, goal);
+            });
+        })
+    }
+
+    pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext<Self>) {
+        if self.take_rename(true, cx).is_some() {
+            return;
+        }
+
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        let row_count = if let Some(row_count) = self.visible_line_count() {
+            row_count as u32 - 1
+        } else {
+            return;
+        };
+
+        let autoscroll = if action.center_cursor {
+            Autoscroll::center()
+        } else {
+            Autoscroll::fit()
+        };
+
+        let text_layout_details = &self.text_layout_details(cx);
+
+        self.change_selections(Some(autoscroll), cx, |s| {
+            let line_mode = s.line_mode;
+            s.move_with(|map, selection| {
+                if !selection.is_empty() && !line_mode {
+                    selection.goal = SelectionGoal::None;
+                }
+                let (cursor, goal) = movement::up_by_rows(
+                    map,
+                    selection.end,
+                    row_count,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
+                selection.collapse_to(cursor, goal);
+            });
+        });
+    }
+
+    pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, goal| {
+                movement::up(map, head, goal, false, &text_layout_details)
+            })
+        })
+    }
+
+    pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
+        self.take_rename(true, cx);
+
+        if self.mode == EditorMode::SingleLine {
+            cx.propagate_action();
+            return;
+        }
+
+        let text_layout_details = &self.text_layout_details(cx);
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            let line_mode = s.line_mode;
+            s.move_with(|map, selection| {
+                if !selection.is_empty() && !line_mode {
+                    selection.goal = SelectionGoal::None;
+                }
+                let (cursor, goal) = movement::down(
+                    map,
+                    selection.end,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
+                selection.collapse_to(cursor, goal);
+            });
+        });
+    }
+
+    pub fn move_page_down(&mut self, action: &MovePageDown, cx: &mut ViewContext<Self>) {
+        if self.take_rename(true, cx).is_some() {
+            return;
+        }
+
+        if self
+            .context_menu
+            .write()
+            .as_mut()
+            .map(|menu| menu.select_last(self.project.as_ref(), cx))
+            .unwrap_or(false)
+        {
+            return;
+        }
+
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        let row_count = if let Some(row_count) = self.visible_line_count() {
+            row_count as u32 - 1
+        } else {
+            return;
+        };
+
+        let autoscroll = if action.center_cursor {
+            Autoscroll::center()
+        } else {
+            Autoscroll::fit()
+        };
+
+        let text_layout_details = &self.text_layout_details(cx);
+        self.change_selections(Some(autoscroll), cx, |s| {
+            let line_mode = s.line_mode;
+            s.move_with(|map, selection| {
+                if !selection.is_empty() && !line_mode {
+                    selection.goal = SelectionGoal::None;
+                }
+                let (cursor, goal) = movement::down_by_rows(
+                    map,
+                    selection.end,
+                    row_count,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
+                selection.collapse_to(cursor, goal);
+            });
+        });
+    }
+
+    pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, goal| {
+                movement::down(map, head, goal, false, &text_layout_details)
+            })
+        });
+    }
+
+    pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.write().as_mut() {
+            context_menu.select_first(self.project.as_ref(), cx);
+        }
+    }
+
+    pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.write().as_mut() {
+            context_menu.select_prev(self.project.as_ref(), cx);
+        }
+    }
+
+    pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.write().as_mut() {
+            context_menu.select_next(self.project.as_ref(), cx);
+        }
+    }
+
+    pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
+        if let Some(context_menu) = self.context_menu.write().as_mut() {
+            context_menu.select_last(self.project.as_ref(), cx);
+        }
+    }
+
+    pub fn move_to_previous_word_start(
+        &mut self,
+        _: &MoveToPreviousWordStart,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_cursors_with(|map, head, _| {
+                (
+                    movement::previous_word_start(map, head),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn move_to_previous_subword_start(
+        &mut self,
+        _: &MoveToPreviousSubwordStart,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_cursors_with(|map, head, _| {
+                (
+                    movement::previous_subword_start(map, head),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn select_to_previous_word_start(
+        &mut self,
+        _: &SelectToPreviousWordStart,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| {
+                (
+                    movement::previous_word_start(map, head),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn select_to_previous_subword_start(
+        &mut self,
+        _: &SelectToPreviousSubwordStart,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| {
+                (
+                    movement::previous_subword_start(map, head),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn delete_to_previous_word_start(
+        &mut self,
+        _: &DeleteToPreviousWordStart,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.transact(cx, |this, cx| {
+            this.select_autoclose_pair(cx);
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                let line_mode = s.line_mode;
+                s.move_with(|map, selection| {
+                    if selection.is_empty() && !line_mode {
+                        let cursor = movement::previous_word_start(map, selection.head());
+                        selection.set_head(cursor, SelectionGoal::None);
+                    }
+                });
+            });
+            this.insert("", cx);
+        });
+    }
+
+    pub fn delete_to_previous_subword_start(
+        &mut self,
+        _: &DeleteToPreviousSubwordStart,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.transact(cx, |this, cx| {
+            this.select_autoclose_pair(cx);
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                let line_mode = s.line_mode;
+                s.move_with(|map, selection| {
+                    if selection.is_empty() && !line_mode {
+                        let cursor = movement::previous_subword_start(map, selection.head());
+                        selection.set_head(cursor, SelectionGoal::None);
+                    }
+                });
+            });
+            this.insert("", cx);
+        });
+    }
+
+    pub fn move_to_next_word_end(&mut self, _: &MoveToNextWordEnd, cx: &mut ViewContext<Self>) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_cursors_with(|map, head, _| {
+                (movement::next_word_end(map, head), SelectionGoal::None)
+            });
+        })
+    }
+
+    pub fn move_to_next_subword_end(
+        &mut self,
+        _: &MoveToNextSubwordEnd,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_cursors_with(|map, head, _| {
+                (movement::next_subword_end(map, head), SelectionGoal::None)
+            });
+        })
+    }
+
+    pub fn select_to_next_word_end(&mut self, _: &SelectToNextWordEnd, cx: &mut ViewContext<Self>) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| {
+                (movement::next_word_end(map, head), SelectionGoal::None)
+            });
+        })
+    }
+
+    pub fn select_to_next_subword_end(
+        &mut self,
+        _: &SelectToNextSubwordEnd,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| {
+                (movement::next_subword_end(map, head), SelectionGoal::None)
+            });
+        })
+    }
+
+    pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext<Self>) {
+        self.transact(cx, |this, cx| {
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                let line_mode = s.line_mode;
+                s.move_with(|map, selection| {
+                    if selection.is_empty() && !line_mode {
+                        let cursor = movement::next_word_end(map, selection.head());
+                        selection.set_head(cursor, SelectionGoal::None);
+                    }
+                });
+            });
+            this.insert("", cx);
+        });
+    }
+
+    pub fn delete_to_next_subword_end(
+        &mut self,
+        _: &DeleteToNextSubwordEnd,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.transact(cx, |this, cx| {
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.move_with(|map, selection| {
+                    if selection.is_empty() {
+                        let cursor = movement::next_subword_end(map, selection.head());
+                        selection.set_head(cursor, SelectionGoal::None);
+                    }
+                });
+            });
+            this.insert("", cx);
+        });
+    }
+
+    pub fn move_to_beginning_of_line(
+        &mut self,
+        _: &MoveToBeginningOfLine,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_cursors_with(|map, head, _| {
+                (
+                    movement::indented_line_beginning(map, head, true),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn select_to_beginning_of_line(
+        &mut self,
+        action: &SelectToBeginningOfLine,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| {
+                (
+                    movement::indented_line_beginning(map, head, action.stop_at_soft_wraps),
+                    SelectionGoal::None,
+                )
+            });
+        });
+    }
+
+    pub fn delete_to_beginning_of_line(
+        &mut self,
+        _: &DeleteToBeginningOfLine,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.transact(cx, |this, cx| {
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.move_with(|_, selection| {
+                    selection.reversed = true;
+                });
+            });
+
+            this.select_to_beginning_of_line(
+                &SelectToBeginningOfLine {
+                    stop_at_soft_wraps: false,
+                },
+                cx,
+            );
+            this.backspace(&Backspace, cx);
+        });
+    }
+
+    pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext<Self>) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_cursors_with(|map, head, _| {
+                (movement::line_end(map, head, true), SelectionGoal::None)
+            });
+        })
+    }
+
+    pub fn select_to_end_of_line(
+        &mut self,
+        action: &SelectToEndOfLine,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| {
+                (
+                    movement::line_end(map, head, action.stop_at_soft_wraps),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext<Self>) {
+        self.transact(cx, |this, cx| {
+            this.select_to_end_of_line(
+                &SelectToEndOfLine {
+                    stop_at_soft_wraps: false,
+                },
+                cx,
+            );
+            this.delete(&Delete, cx);
+        });
+    }
+
+    pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext<Self>) {
+        self.transact(cx, |this, cx| {
+            this.select_to_end_of_line(
+                &SelectToEndOfLine {
+                    stop_at_soft_wraps: false,
+                },
+                cx,
+            );
+            this.cut(&Cut, cx);
+        });
+    }
+
+    pub fn move_to_start_of_paragraph(
+        &mut self,
+        _: &MoveToStartOfParagraph,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_with(|map, selection| {
+                selection.collapse_to(
+                    movement::start_of_paragraph(map, selection.head(), 1),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn move_to_end_of_paragraph(
+        &mut self,
+        _: &MoveToEndOfParagraph,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_with(|map, selection| {
+                selection.collapse_to(
+                    movement::end_of_paragraph(map, selection.head(), 1),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn select_to_start_of_paragraph(
+        &mut self,
+        _: &SelectToStartOfParagraph,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| {
+                (
+                    movement::start_of_paragraph(map, head, 1),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn select_to_end_of_paragraph(
+        &mut self,
+        _: &SelectToEndOfParagraph,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| {
+                (
+                    movement::end_of_paragraph(map, head, 1),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.select_ranges(vec![0..0]);
+        });
+    }
+
+    pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext<Self>) {
+        let mut selection = self.selections.last::<Point>(cx);
+        selection.set_head(Point::zero(), SelectionGoal::None);
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.select(vec![selection]);
+        });
+    }
+
+    pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext<Self>) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        let cursor = self.buffer.read(cx).read(cx).len();
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.select_ranges(vec![cursor..cursor])
+        });
+    }
+
+    pub fn set_nav_history(&mut self, nav_history: Option<ItemNavHistory>) {
+        self.nav_history = nav_history;
+    }
+
+    pub fn nav_history(&self) -> Option<&ItemNavHistory> {
+        self.nav_history.as_ref()
+    }
+
+    fn push_to_nav_history(
+        &mut self,
+        cursor_anchor: Anchor,
+        new_position: Option<Point>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(nav_history) = self.nav_history.as_mut() {
+            let buffer = self.buffer.read(cx).read(cx);
+            let cursor_position = cursor_anchor.to_point(&buffer);
+            let scroll_state = self.scroll_manager.anchor();
+            let scroll_top_row = scroll_state.top_row(&buffer);
+            drop(buffer);
+
+            if let Some(new_position) = new_position {
+                let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs();
+                if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA {
+                    return;
+                }
+            }
+
+            nav_history.push(
+                Some(NavigationData {
+                    cursor_anchor,
+                    cursor_position,
+                    scroll_anchor: scroll_state,
+                    scroll_top_row,
+                }),
+                cx,
+            );
+        }
+    }
+
+    pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext<Self>) {
+        let buffer = self.buffer.read(cx).snapshot(cx);
+        let mut selection = self.selections.first::<usize>(cx);
+        selection.set_head(buffer.len(), SelectionGoal::None);
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.select(vec![selection]);
+        });
+    }
+
+    pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
+        let end = self.buffer.read(cx).read(cx).len();
+        self.change_selections(None, cx, |s| {
+            s.select_ranges(vec![0..end]);
+        });
+    }
+
+    pub fn select_line(&mut self, _: &SelectLine, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let mut selections = self.selections.all::<Point>(cx);
+        let max_point = display_map.buffer_snapshot.max_point();
+        for selection in &mut selections {
+            let rows = selection.spanned_rows(true, &display_map);
+            selection.start = Point::new(rows.start, 0);
+            selection.end = cmp::min(max_point, Point::new(rows.end, 0));
+            selection.reversed = false;
+        }
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.select(selections);
+        });
+    }
+
+    pub fn split_selection_into_lines(
+        &mut self,
+        _: &SplitSelectionIntoLines,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let mut to_unfold = Vec::new();
+        let mut new_selection_ranges = Vec::new();
+        {
+            let selections = self.selections.all::<Point>(cx);
+            let buffer = self.buffer.read(cx).read(cx);
+            for selection in selections {
+                for row in selection.start.row..selection.end.row {
+                    let cursor = Point::new(row, buffer.line_len(row));
+                    new_selection_ranges.push(cursor..cursor);
+                }
+                new_selection_ranges.push(selection.end..selection.end);
+                to_unfold.push(selection.start..selection.end);
+            }
+        }
+        self.unfold_ranges(to_unfold, true, true, cx);
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.select_ranges(new_selection_ranges);
+        });
+    }
+
+    pub fn add_selection_above(&mut self, _: &AddSelectionAbove, cx: &mut ViewContext<Self>) {
+        self.add_selection(true, cx);
+    }
+
+    pub fn add_selection_below(&mut self, _: &AddSelectionBelow, cx: &mut ViewContext<Self>) {
+        self.add_selection(false, cx);
+    }
+
+    fn add_selection(&mut self, above: bool, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let mut selections = self.selections.all::<Point>(cx);
+        let text_layout_details = self.text_layout_details(cx);
+        let mut state = self.add_selections_state.take().unwrap_or_else(|| {
+            let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
+            let range = oldest_selection.display_range(&display_map).sorted();
+
+            let start_x = display_map.x_for_point(range.start, &text_layout_details);
+            let end_x = display_map.x_for_point(range.end, &text_layout_details);
+            let positions = start_x.min(end_x)..start_x.max(end_x);
+
+            selections.clear();
+            let mut stack = Vec::new();
+            for row in range.start.row()..=range.end.row() {
+                if let Some(selection) = self.selections.build_columnar_selection(
+                    &display_map,
+                    row,
+                    &positions,
+                    oldest_selection.reversed,
+                    &text_layout_details,
+                ) {
+                    stack.push(selection.id);
+                    selections.push(selection);
+                }
+            }
+
+            if above {
+                stack.reverse();
+            }
+
+            AddSelectionsState { above, stack }
+        });
+
+        let last_added_selection = *state.stack.last().unwrap();
+        let mut new_selections = Vec::new();
+        if above == state.above {
+            let end_row = if above {
+                0
+            } else {
+                display_map.max_point().row()
+            };
+
+            'outer: for selection in selections {
+                if selection.id == last_added_selection {
+                    let range = selection.display_range(&display_map).sorted();
+                    debug_assert_eq!(range.start.row(), range.end.row());
+                    let mut row = range.start.row();
+                    let positions = if let SelectionGoal::HorizontalRange { start, end } =
+                        selection.goal
+                    {
+                        start..end
+                    } else {
+                        let start_x = display_map.x_for_point(range.start, &text_layout_details);
+                        let end_x = display_map.x_for_point(range.end, &text_layout_details);
+
+                        start_x.min(end_x)..start_x.max(end_x)
+                    };
+
+                    while row != end_row {
+                        if above {
+                            row -= 1;
+                        } else {
+                            row += 1;
+                        }
+
+                        if let Some(new_selection) = self.selections.build_columnar_selection(
+                            &display_map,
+                            row,
+                            &positions,
+                            selection.reversed,
+                            &text_layout_details,
+                        ) {
+                            state.stack.push(new_selection.id);
+                            if above {
+                                new_selections.push(new_selection);
+                                new_selections.push(selection);
+                            } else {
+                                new_selections.push(selection);
+                                new_selections.push(new_selection);
+                            }
+
+                            continue 'outer;
+                        }
+                    }
+                }
+
+                new_selections.push(selection);
+            }
+        } else {
+            new_selections = selections;
+            new_selections.retain(|s| s.id != last_added_selection);
+            state.stack.pop();
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.select(new_selections);
+        });
+        if state.stack.len() > 1 {
+            self.add_selections_state = Some(state);
+        }
+    }
+
+    pub fn select_next_match_internal(
+        &mut self,
+        display_map: &DisplaySnapshot,
+        replace_newest: bool,
+        autoscroll: Option<Autoscroll>,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()> {
+        fn select_next_match_ranges(
+            this: &mut Editor,
+            range: Range<usize>,
+            replace_newest: bool,
+            auto_scroll: Option<Autoscroll>,
+            cx: &mut ViewContext<Editor>,
+        ) {
+            this.unfold_ranges([range.clone()], false, true, cx);
+            this.change_selections(auto_scroll, cx, |s| {
+                if replace_newest {
+                    s.delete(s.newest_anchor().id);
+                }
+                s.insert_range(range.clone());
+            });
+        }
+
+        let buffer = &display_map.buffer_snapshot;
+        let mut selections = self.selections.all::<usize>(cx);
+        if let Some(mut select_next_state) = self.select_next_state.take() {
+            let query = &select_next_state.query;
+            if !select_next_state.done {
+                let first_selection = selections.iter().min_by_key(|s| s.id).unwrap();
+                let last_selection = selections.iter().max_by_key(|s| s.id).unwrap();
+                let mut next_selected_range = None;
+
+                let bytes_after_last_selection =
+                    buffer.bytes_in_range(last_selection.end..buffer.len());
+                let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start);
+                let query_matches = query
+                    .stream_find_iter(bytes_after_last_selection)
+                    .map(|result| (last_selection.end, result))
+                    .chain(
+                        query
+                            .stream_find_iter(bytes_before_first_selection)
+                            .map(|result| (0, result)),
+                    );
+
+                for (start_offset, query_match) in query_matches {
+                    let query_match = query_match.unwrap(); // can only fail due to I/O
+                    let offset_range =
+                        start_offset + query_match.start()..start_offset + query_match.end();
+                    let display_range = offset_range.start.to_display_point(&display_map)
+                        ..offset_range.end.to_display_point(&display_map);
+
+                    if !select_next_state.wordwise
+                        || (!movement::is_inside_word(&display_map, display_range.start)
+                            && !movement::is_inside_word(&display_map, display_range.end))
+                    {
+                        if selections
+                            .iter()
+                            .find(|selection| selection.range().overlaps(&offset_range))
+                            .is_none()
+                        {
+                            next_selected_range = Some(offset_range);
+                            break;
+                        }
+                    }
+                }
+
+                if let Some(next_selected_range) = next_selected_range {
+                    select_next_match_ranges(
+                        self,
+                        next_selected_range,
+                        replace_newest,
+                        autoscroll,
+                        cx,
+                    );
+                } else {
+                    select_next_state.done = true;
+                }
+            }
+
+            self.select_next_state = Some(select_next_state);
+        } else if selections.len() == 1 {
+            let selection = selections.last_mut().unwrap();
+            if selection.start == selection.end {
+                let word_range = movement::surrounding_word(
+                    &display_map,
+                    selection.start.to_display_point(&display_map),
+                );
+                selection.start = word_range.start.to_offset(&display_map, Bias::Left);
+                selection.end = word_range.end.to_offset(&display_map, Bias::Left);
+                selection.goal = SelectionGoal::None;
+                selection.reversed = false;
+
+                let query = buffer
+                    .text_for_range(selection.start..selection.end)
+                    .collect::<String>();
+
+                let is_empty = query.is_empty();
+                let select_state = SelectNextState {
+                    query: AhoCorasick::new(&[query])?,
+                    wordwise: true,
+                    done: is_empty,
+                };
+                select_next_match_ranges(
+                    self,
+                    selection.start..selection.end,
+                    replace_newest,
+                    autoscroll,
+                    cx,
+                );
+                self.select_next_state = Some(select_state);
+            } else {
+                let query = buffer
+                    .text_for_range(selection.start..selection.end)
+                    .collect::<String>();
+                self.select_next_state = Some(SelectNextState {
+                    query: AhoCorasick::new(&[query])?,
+                    wordwise: false,
+                    done: false,
+                });
+                self.select_next_match_internal(display_map, replace_newest, autoscroll, cx)?;
+            }
+        }
+        Ok(())
+    }
+
+    pub fn select_all_matches(
+        &mut self,
+        action: &SelectAllMatches,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()> {
+        self.push_to_selection_history();
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        loop {
+            self.select_next_match_internal(&display_map, action.replace_newest, None, cx)?;
+
+            if self
+                .select_next_state
+                .as_ref()
+                .map(|selection_state| selection_state.done)
+                .unwrap_or(true)
+            {
+                break;
+            }
+        }
+
+        Ok(())
+    }
+
+    pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext<Self>) -> Result<()> {
+        self.push_to_selection_history();
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        self.select_next_match_internal(
+            &display_map,
+            action.replace_newest,
+            Some(Autoscroll::newest()),
+            cx,
+        )?;
+        Ok(())
+    }
+
+    pub fn select_previous(
+        &mut self,
+        action: &SelectPrevious,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()> {
+        self.push_to_selection_history();
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = &display_map.buffer_snapshot;
+        let mut selections = self.selections.all::<usize>(cx);
+        if let Some(mut select_prev_state) = self.select_prev_state.take() {
+            let query = &select_prev_state.query;
+            if !select_prev_state.done {
+                let first_selection = selections.iter().min_by_key(|s| s.id).unwrap();
+                let last_selection = selections.iter().max_by_key(|s| s.id).unwrap();
+                let mut next_selected_range = None;
+                // When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer.
+                let bytes_before_last_selection =
+                    buffer.reversed_bytes_in_range(0..last_selection.start);
+                let bytes_after_first_selection =
+                    buffer.reversed_bytes_in_range(first_selection.end..buffer.len());
+                let query_matches = query
+                    .stream_find_iter(bytes_before_last_selection)
+                    .map(|result| (last_selection.start, result))
+                    .chain(
+                        query
+                            .stream_find_iter(bytes_after_first_selection)
+                            .map(|result| (buffer.len(), result)),
+                    );
+                for (end_offset, query_match) in query_matches {
+                    let query_match = query_match.unwrap(); // can only fail due to I/O
+                    let offset_range =
+                        end_offset - query_match.end()..end_offset - query_match.start();
+                    let display_range = offset_range.start.to_display_point(&display_map)
+                        ..offset_range.end.to_display_point(&display_map);
+
+                    if !select_prev_state.wordwise
+                        || (!movement::is_inside_word(&display_map, display_range.start)
+                            && !movement::is_inside_word(&display_map, display_range.end))
+                    {
+                        next_selected_range = Some(offset_range);
+                        break;
+                    }
+                }
+
+                if let Some(next_selected_range) = next_selected_range {
+                    self.unfold_ranges([next_selected_range.clone()], false, true, cx);
+                    self.change_selections(Some(Autoscroll::newest()), cx, |s| {
+                        if action.replace_newest {
+                            s.delete(s.newest_anchor().id);
+                        }
+                        s.insert_range(next_selected_range);
+                    });
+                } else {
+                    select_prev_state.done = true;
+                }
+            }
+
+            self.select_prev_state = Some(select_prev_state);
+        } else if selections.len() == 1 {
+            let selection = selections.last_mut().unwrap();
+            if selection.start == selection.end {
+                let word_range = movement::surrounding_word(
+                    &display_map,
+                    selection.start.to_display_point(&display_map),
+                );
+                selection.start = word_range.start.to_offset(&display_map, Bias::Left);
+                selection.end = word_range.end.to_offset(&display_map, Bias::Left);
+                selection.goal = SelectionGoal::None;
+                selection.reversed = false;
+
+                let query = buffer
+                    .text_for_range(selection.start..selection.end)
+                    .collect::<String>();
+                let query = query.chars().rev().collect::<String>();
+                let select_state = SelectNextState {
+                    query: AhoCorasick::new(&[query])?,
+                    wordwise: true,
+                    done: false,
+                };
+                self.unfold_ranges([selection.start..selection.end], false, true, cx);
+                self.change_selections(Some(Autoscroll::newest()), cx, |s| {
+                    s.select(selections);
+                });
+                self.select_prev_state = Some(select_state);
+            } else {
+                let query = buffer
+                    .text_for_range(selection.start..selection.end)
+                    .collect::<String>();
+                let query = query.chars().rev().collect::<String>();
+                self.select_prev_state = Some(SelectNextState {
+                    query: AhoCorasick::new(&[query])?,
+                    wordwise: false,
+                    done: false,
+                });
+                self.select_previous(action, cx)?;
+            }
+        }
+        Ok(())
+    }
+
+    pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
+        self.transact(cx, |this, cx| {
+            let mut selections = this.selections.all::<Point>(cx);
+            let mut edits = Vec::new();
+            let mut selection_edit_ranges = Vec::new();
+            let mut last_toggled_row = None;
+            let snapshot = this.buffer.read(cx).read(cx);
+            let empty_str: Arc<str> = "".into();
+            let mut suffixes_inserted = Vec::new();
+
+            fn comment_prefix_range(
+                snapshot: &MultiBufferSnapshot,
+                row: u32,
+                comment_prefix: &str,
+                comment_prefix_whitespace: &str,
+            ) -> Range<Point> {
+                let start = Point::new(row, snapshot.indent_size_for_line(row).len);
+
+                let mut line_bytes = snapshot
+                    .bytes_in_range(start..snapshot.max_point())
+                    .flatten()
+                    .copied();
+
+                // If this line currently begins with the line comment prefix, then record
+                // the range containing the prefix.
+                if line_bytes
+                    .by_ref()
+                    .take(comment_prefix.len())
+                    .eq(comment_prefix.bytes())
+                {
+                    // Include any whitespace that matches the comment prefix.
+                    let matching_whitespace_len = line_bytes
+                        .zip(comment_prefix_whitespace.bytes())
+                        .take_while(|(a, b)| a == b)
+                        .count() as u32;
+                    let end = Point::new(
+                        start.row,
+                        start.column + comment_prefix.len() as u32 + matching_whitespace_len,
+                    );
+                    start..end
+                } else {
+                    start..start
+                }
+            }
+
+            fn comment_suffix_range(
+                snapshot: &MultiBufferSnapshot,
+                row: u32,
+                comment_suffix: &str,
+                comment_suffix_has_leading_space: bool,
+            ) -> Range<Point> {
+                let end = Point::new(row, snapshot.line_len(row));
+                let suffix_start_column = end.column.saturating_sub(comment_suffix.len() as u32);
+
+                let mut line_end_bytes = snapshot
+                    .bytes_in_range(Point::new(end.row, suffix_start_column.saturating_sub(1))..end)
+                    .flatten()
+                    .copied();
+
+                let leading_space_len = if suffix_start_column > 0
+                    && line_end_bytes.next() == Some(b' ')
+                    && comment_suffix_has_leading_space
+                {
+                    1
+                } else {
+                    0
+                };
+
+                // If this line currently begins with the line comment prefix, then record
+                // the range containing the prefix.
+                if line_end_bytes.by_ref().eq(comment_suffix.bytes()) {
+                    let start = Point::new(end.row, suffix_start_column - leading_space_len);
+                    start..end
+                } else {
+                    end..end
+                }
+            }
+
+            // TODO: Handle selections that cross excerpts
+            for selection in &mut selections {
+                let start_column = snapshot.indent_size_for_line(selection.start.row).len;
+                let language = if let Some(language) =
+                    snapshot.language_scope_at(Point::new(selection.start.row, start_column))
+                {
+                    language
+                } else {
+                    continue;
+                };
+
+                selection_edit_ranges.clear();
+
+                // If multiple selections contain a given row, avoid processing that
+                // row more than once.
+                let mut start_row = selection.start.row;
+                if last_toggled_row == Some(start_row) {
+                    start_row += 1;
+                }
+                let end_row =
+                    if selection.end.row > selection.start.row && selection.end.column == 0 {
+                        selection.end.row - 1
+                    } else {
+                        selection.end.row
+                    };
+                last_toggled_row = Some(end_row);
+
+                if start_row > end_row {
+                    continue;
+                }
+
+                // If the language has line comments, toggle those.
+                if let Some(full_comment_prefix) = language.line_comment_prefix() {
+                    // Split the comment prefix's trailing whitespace into a separate string,
+                    // as that portion won't be used for detecting if a line is a comment.
+                    let comment_prefix = full_comment_prefix.trim_end_matches(' ');
+                    let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
+                    let mut all_selection_lines_are_comments = true;
+
+                    for row in start_row..=end_row {
+                        if snapshot.is_line_blank(row) && start_row < end_row {
+                            continue;
+                        }
+
+                        let prefix_range = comment_prefix_range(
+                            snapshot.deref(),
+                            row,
+                            comment_prefix,
+                            comment_prefix_whitespace,
+                        );
+                        if prefix_range.is_empty() {
+                            all_selection_lines_are_comments = false;
+                        }
+                        selection_edit_ranges.push(prefix_range);
+                    }
+
+                    if all_selection_lines_are_comments {
+                        edits.extend(
+                            selection_edit_ranges
+                                .iter()
+                                .cloned()
+                                .map(|range| (range, empty_str.clone())),
+                        );
+                    } else {
+                        let min_column = selection_edit_ranges
+                            .iter()
+                            .map(|r| r.start.column)
+                            .min()
+                            .unwrap_or(0);
+                        edits.extend(selection_edit_ranges.iter().map(|range| {
+                            let position = Point::new(range.start.row, min_column);
+                            (position..position, full_comment_prefix.clone())
+                        }));
+                    }
+                } else if let Some((full_comment_prefix, comment_suffix)) =
+                    language.block_comment_delimiters()
+                {
+                    let comment_prefix = full_comment_prefix.trim_end_matches(' ');
+                    let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
+                    let prefix_range = comment_prefix_range(
+                        snapshot.deref(),
+                        start_row,
+                        comment_prefix,
+                        comment_prefix_whitespace,
+                    );
+                    let suffix_range = comment_suffix_range(
+                        snapshot.deref(),
+                        end_row,
+                        comment_suffix.trim_start_matches(' '),
+                        comment_suffix.starts_with(' '),
+                    );
+
+                    if prefix_range.is_empty() || suffix_range.is_empty() {
+                        edits.push((
+                            prefix_range.start..prefix_range.start,
+                            full_comment_prefix.clone(),
+                        ));
+                        edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone()));
+                        suffixes_inserted.push((end_row, comment_suffix.len()));
+                    } else {
+                        edits.push((prefix_range, empty_str.clone()));
+                        edits.push((suffix_range, empty_str.clone()));
+                    }
+                } else {
+                    continue;
+                }
+            }
+
+            drop(snapshot);
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, None, cx);
+            });
+
+            // Adjust selections so that they end before any comment suffixes that
+            // were inserted.
+            let mut suffixes_inserted = suffixes_inserted.into_iter().peekable();
+            let mut selections = this.selections.all::<Point>(cx);
+            let snapshot = this.buffer.read(cx).read(cx);
+            for selection in &mut selections {
+                while let Some((row, suffix_len)) = suffixes_inserted.peek().copied() {
+                    match row.cmp(&selection.end.row) {
+                        Ordering::Less => {
+                            suffixes_inserted.next();
+                            continue;
+                        }
+                        Ordering::Greater => break,
+                        Ordering::Equal => {
+                            if selection.end.column == snapshot.line_len(row) {
+                                if selection.is_empty() {
+                                    selection.start.column -= suffix_len as u32;
+                                }
+                                selection.end.column -= suffix_len as u32;
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+
+            drop(snapshot);
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+
+            let selections = this.selections.all::<Point>(cx);
+            let selections_on_single_row = selections.windows(2).all(|selections| {
+                selections[0].start.row == selections[1].start.row
+                    && selections[0].end.row == selections[1].end.row
+                    && selections[0].start.row == selections[0].end.row
+            });
+            let selections_selecting = selections
+                .iter()
+                .any(|selection| selection.start != selection.end);
+            let advance_downwards = action.advance_downwards
+                && selections_on_single_row
+                && !selections_selecting
+                && this.mode != EditorMode::SingleLine;
+
+            if advance_downwards {
+                let snapshot = this.buffer.read(cx).snapshot(cx);
+
+                this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.move_cursors_with(|display_snapshot, display_point, _| {
+                        let mut point = display_point.to_point(display_snapshot);
+                        point.row += 1;
+                        point = snapshot.clip_point(point, Bias::Left);
+                        let display_point = point.to_display_point(display_snapshot);
+                        let goal = SelectionGoal::HorizontalPosition(
+                            display_snapshot.x_for_point(display_point, &text_layout_details),
+                        );
+                        (display_point, goal)
+                    })
+                });
+            }
+        });
+    }
+
+    pub fn select_larger_syntax_node(
+        &mut self,
+        _: &SelectLargerSyntaxNode,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = self.buffer.read(cx).snapshot(cx);
+        let old_selections = self.selections.all::<usize>(cx).into_boxed_slice();
+
+        let mut stack = mem::take(&mut self.select_larger_syntax_node_stack);
+        let mut selected_larger_node = false;
+        let new_selections = old_selections
+            .iter()
+            .map(|selection| {
+                let old_range = selection.start..selection.end;
+                let mut new_range = old_range.clone();
+                while let Some(containing_range) =
+                    buffer.range_for_syntax_ancestor(new_range.clone())
+                {
+                    new_range = containing_range;
+                    if !display_map.intersects_fold(new_range.start)
+                        && !display_map.intersects_fold(new_range.end)
+                    {
+                        break;
+                    }
+                }
+
+                selected_larger_node |= new_range != old_range;
+                Selection {
+                    id: selection.id,
+                    start: new_range.start,
+                    end: new_range.end,
+                    goal: SelectionGoal::None,
+                    reversed: selection.reversed,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        if selected_larger_node {
+            stack.push(old_selections);
+            self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(new_selections);
+            });
+        }
+        self.select_larger_syntax_node_stack = stack;
+    }
+
+    pub fn select_smaller_syntax_node(
+        &mut self,
+        _: &SelectSmallerSyntaxNode,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let mut stack = mem::take(&mut self.select_larger_syntax_node_stack);
+        if let Some(selections) = stack.pop() {
+            self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(selections.to_vec());
+            });
+        }
+        self.select_larger_syntax_node_stack = stack;
+    }
+
+    pub fn move_to_enclosing_bracket(
+        &mut self,
+        _: &MoveToEnclosingBracket,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_offsets_with(|snapshot, selection| {
+                let Some(enclosing_bracket_ranges) =
+                    snapshot.enclosing_bracket_ranges(selection.start..selection.end)
+                else {
+                    return;
+                };
+
+                let mut best_length = usize::MAX;
+                let mut best_inside = false;
+                let mut best_in_bracket_range = false;
+                let mut best_destination = None;
+                for (open, close) in enclosing_bracket_ranges {
+                    let close = close.to_inclusive();
+                    let length = close.end() - open.start;
+                    let inside = selection.start >= open.end && selection.end <= *close.start();
+                    let in_bracket_range = open.to_inclusive().contains(&selection.head())
+                        || close.contains(&selection.head());
+
+                    // If best is next to a bracket and current isn't, skip
+                    if !in_bracket_range && best_in_bracket_range {
+                        continue;
+                    }
+
+                    // Prefer smaller lengths unless best is inside and current isn't
+                    if length > best_length && (best_inside || !inside) {
+                        continue;
+                    }
+
+                    best_length = length;
+                    best_inside = inside;
+                    best_in_bracket_range = in_bracket_range;
+                    best_destination = Some(
+                        if close.contains(&selection.start) && close.contains(&selection.end) {
+                            if inside {
+                                open.end
+                            } else {
+                                open.start
+                            }
+                        } else {
+                            if inside {
+                                *close.start()
+                            } else {
+                                *close.end()
+                            }
+                        },
+                    );
+                }
+
+                if let Some(destination) = best_destination {
+                    selection.collapse_to(destination, SelectionGoal::None);
+                }
+            })
+        });
+    }
+
+    pub fn undo_selection(&mut self, _: &UndoSelection, cx: &mut ViewContext<Self>) {
+        self.end_selection(cx);
+        self.selection_history.mode = SelectionHistoryMode::Undoing;
+        if let Some(entry) = self.selection_history.undo_stack.pop_back() {
+            self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
+            self.select_next_state = entry.select_next_state;
+            self.select_prev_state = entry.select_prev_state;
+            self.add_selections_state = entry.add_selections_state;
+            self.request_autoscroll(Autoscroll::newest(), cx);
+        }
+        self.selection_history.mode = SelectionHistoryMode::Normal;
+    }
+
+    pub fn redo_selection(&mut self, _: &RedoSelection, cx: &mut ViewContext<Self>) {
+        self.end_selection(cx);
+        self.selection_history.mode = SelectionHistoryMode::Redoing;
+        if let Some(entry) = self.selection_history.redo_stack.pop_back() {
+            self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
+            self.select_next_state = entry.select_next_state;
+            self.select_prev_state = entry.select_prev_state;
+            self.add_selections_state = entry.add_selections_state;
+            self.request_autoscroll(Autoscroll::newest(), cx);
+        }
+        self.selection_history.mode = SelectionHistoryMode::Normal;
+    }
+
+    fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
+        self.go_to_diagnostic_impl(Direction::Next, cx)
+    }
+
+    fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext<Self>) {
+        self.go_to_diagnostic_impl(Direction::Prev, cx)
+    }
+
+    pub fn go_to_diagnostic_impl(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+        let buffer = self.buffer.read(cx).snapshot(cx);
+        let selection = self.selections.newest::<usize>(cx);
+
+        // If there is an active Diagnostic Popover. Jump to it's diagnostic instead.
+        if direction == Direction::Next {
+            if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
+                let (group_id, jump_to) = popover.activation_info();
+                if self.activate_diagnostics(group_id, cx) {
+                    self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                        let mut new_selection = s.newest_anchor().clone();
+                        new_selection.collapse_to(jump_to, SelectionGoal::None);
+                        s.select_anchors(vec![new_selection.clone()]);
+                    });
+                }
+                return;
+            }
+        }
+
+        let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
+            active_diagnostics
+                .primary_range
+                .to_offset(&buffer)
+                .to_inclusive()
+        });
+        let mut search_start = if let Some(active_primary_range) = active_primary_range.as_ref() {
+            if active_primary_range.contains(&selection.head()) {
+                *active_primary_range.end()
+            } else {
+                selection.head()
+            }
+        } else {
+            selection.head()
+        };
+
+        loop {
+            let mut diagnostics = if direction == Direction::Prev {
+                buffer.diagnostics_in_range::<_, usize>(0..search_start, true)
+            } else {
+                buffer.diagnostics_in_range::<_, usize>(search_start..buffer.len(), false)
+            };
+            let group = diagnostics.find_map(|entry| {
+                if entry.diagnostic.is_primary
+                    && entry.diagnostic.severity <= DiagnosticSeverity::WARNING
+                    && !entry.range.is_empty()
+                    && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end())
+                    && !entry.range.contains(&search_start)
+                {
+                    Some((entry.range, entry.diagnostic.group_id))
+                } else {
+                    None
+                }
+            });
+
+            if let Some((primary_range, group_id)) = group {
+                if self.activate_diagnostics(group_id, cx) {
+                    self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                        s.select(vec![Selection {
+                            id: selection.id,
+                            start: primary_range.start,
+                            end: primary_range.start,
+                            reversed: false,
+                            goal: SelectionGoal::None,
+                        }]);
+                    });
+                }
+                break;
+            } else {
+                // Cycle around to the start of the buffer, potentially moving back to the start of
+                // the currently active diagnostic.
+                active_primary_range.take();
+                if direction == Direction::Prev {
+                    if search_start == buffer.len() {
+                        break;
+                    } else {
+                        search_start = buffer.len();
+                    }
+                } else if search_start == 0 {
+                    break;
+                } else {
+                    search_start = 0;
+                }
+            }
+        }
+    }
+
+    fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
+        let snapshot = self
+            .display_map
+            .update(cx, |display_map, cx| display_map.snapshot(cx));
+        let selection = self.selections.newest::<Point>(cx);
+
+        if !self.seek_in_direction(
+            &snapshot,
+            selection.head(),
+            false,
+            snapshot
+                .buffer_snapshot
+                .git_diff_hunks_in_range((selection.head().row + 1)..u32::MAX),
+            cx,
+        ) {
+            let wrapped_point = Point::zero();
+            self.seek_in_direction(
+                &snapshot,
+                wrapped_point,
+                true,
+                snapshot
+                    .buffer_snapshot
+                    .git_diff_hunks_in_range((wrapped_point.row + 1)..u32::MAX),
+                cx,
+            );
+        }
+    }
+
+    fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
+        let snapshot = self
+            .display_map
+            .update(cx, |display_map, cx| display_map.snapshot(cx));
+        let selection = self.selections.newest::<Point>(cx);
+
+        if !self.seek_in_direction(
+            &snapshot,
+            selection.head(),
+            false,
+            snapshot
+                .buffer_snapshot
+                .git_diff_hunks_in_range_rev(0..selection.head().row),
+            cx,
+        ) {
+            let wrapped_point = snapshot.buffer_snapshot.max_point();
+            self.seek_in_direction(
+                &snapshot,
+                wrapped_point,
+                true,
+                snapshot
+                    .buffer_snapshot
+                    .git_diff_hunks_in_range_rev(0..wrapped_point.row),
+                cx,
+            );
+        }
+    }
+
+    fn seek_in_direction(
+        &mut self,
+        snapshot: &DisplaySnapshot,
+        initial_point: Point,
+        is_wrapped: bool,
+        hunks: impl Iterator<Item = DiffHunk<u32>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        let display_point = initial_point.to_display_point(snapshot);
+        let mut hunks = hunks
+            .map(|hunk| diff_hunk_to_display(hunk, &snapshot))
+            .filter(|hunk| {
+                if is_wrapped {
+                    true
+                } else {
+                    !hunk.contains_display_row(display_point.row())
+                }
+            })
+            .dedup();
+
+        if let Some(hunk) = hunks.next() {
+            self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                let row = hunk.start_display_row();
+                let point = DisplayPoint::new(row, 0);
+                s.select_display_ranges([point..point]);
+            });
+
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn go_to_definition(&mut self, _: &GoToDefinition, cx: &mut ViewContext<Self>) {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx);
+    }
+
+    pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext<Self>) {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx);
+    }
+
+    pub fn go_to_definition_split(&mut self, _: &GoToDefinitionSplit, cx: &mut ViewContext<Self>) {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, cx);
+    }
+
+    pub fn go_to_type_definition_split(
+        &mut self,
+        _: &GoToTypeDefinitionSplit,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, cx);
+    }
+
+    fn go_to_definition_of_kind(
+        &mut self,
+        kind: GotoDefinitionKind,
+        split: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let Some(workspace) = self.workspace(cx) else {
+            return;
+        };
+        let buffer = self.buffer.read(cx);
+        let head = self.selections.newest::<usize>(cx).head();
+        let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) {
+            text_anchor
+        } else {
+            return;
+        };
+
+        let project = workspace.read(cx).project().clone();
+        let definitions = project.update(cx, |project, cx| match kind {
+            GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
+            GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
+        });
+
+        cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move {
+            let definitions = definitions.await?;
+            editor.update(&mut cx, |editor, cx| {
+                editor.navigate_to_definitions(
+                    definitions
+                        .into_iter()
+                        .map(GoToDefinitionLink::Text)
+                        .collect(),
+                    split,
+                    cx,
+                );
+            })?;
+            Ok::<(), anyhow::Error>(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    pub fn navigate_to_definitions(
+        &mut self,
+        mut definitions: Vec<GoToDefinitionLink>,
+        split: bool,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let Some(workspace) = self.workspace(cx) else {
+            return;
+        };
+        let pane = workspace.read(cx).active_pane().clone();
+        // If there is one definition, just open it directly
+        if definitions.len() == 1 {
+            let definition = definitions.pop().unwrap();
+            let target_task = match definition {
+                GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
+                GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
+                    self.compute_target_location(lsp_location, server_id, cx)
+                }
+            };
+            cx.spawn(|editor, mut cx| async move {
+                let target = target_task.await.context("target resolution task")?;
+                if let Some(target) = target {
+                    editor.update(&mut cx, |editor, cx| {
+                        let range = target.range.to_offset(target.buffer.read(cx));
+                        let range = editor.range_for_match(&range);
+                        if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() {
+                            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                                s.select_ranges([range]);
+                            });
+                        } else {
+                            cx.window_context().defer(move |cx| {
+                                let target_editor: ViewHandle<Self> =
+                                    workspace.update(cx, |workspace, cx| {
+                                        if split {
+                                            workspace.split_project_item(target.buffer.clone(), cx)
+                                        } else {
+                                            workspace.open_project_item(target.buffer.clone(), cx)
+                                        }
+                                    });
+                                target_editor.update(cx, |target_editor, cx| {
+                                    // When selecting a definition in a different buffer, disable the nav history
+                                    // to avoid creating a history entry at the previous cursor location.
+                                    pane.update(cx, |pane, _| pane.disable_history());
+                                    target_editor.change_selections(
+                                        Some(Autoscroll::fit()),
+                                        cx,
+                                        |s| {
+                                            s.select_ranges([range]);
+                                        },
+                                    );
+                                    pane.update(cx, |pane, _| pane.enable_history());
+                                });
+                            });
+                        }
+                    })
+                } else {
+                    Ok(())
+                }
+            })
+            .detach_and_log_err(cx);
+        } else if !definitions.is_empty() {
+            let replica_id = self.replica_id(cx);
+            cx.spawn(|editor, mut cx| async move {
+                let (title, location_tasks) = editor
+                    .update(&mut cx, |editor, cx| {
+                        let title = definitions
+                            .iter()
+                            .find_map(|definition| match definition {
+                                GoToDefinitionLink::Text(link) => {
+                                    link.origin.as_ref().map(|origin| {
+                                        let buffer = origin.buffer.read(cx);
+                                        format!(
+                                            "Definitions for {}",
+                                            buffer
+                                                .text_for_range(origin.range.clone())
+                                                .collect::<String>()
+                                        )
+                                    })
+                                }
+                                GoToDefinitionLink::InlayHint(_, _) => None,
+                            })
+                            .unwrap_or("Definitions".to_string());
+                        let location_tasks = definitions
+                            .into_iter()
+                            .map(|definition| match definition {
+                                GoToDefinitionLink::Text(link) => {
+                                    Task::Ready(Some(Ok(Some(link.target))))
+                                }
+                                GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
+                                    editor.compute_target_location(lsp_location, server_id, cx)
+                                }
+                            })
+                            .collect::<Vec<_>>();
+                        (title, location_tasks)
+                    })
+                    .context("location tasks preparation")?;
+
+                let locations = futures::future::join_all(location_tasks)
+                    .await
+                    .into_iter()
+                    .filter_map(|location| location.transpose())
+                    .collect::<Result<_>>()
+                    .context("location tasks")?;
+                workspace.update(&mut cx, |workspace, cx| {
+                    Self::open_locations_in_multibuffer(
+                        workspace, locations, replica_id, title, split, cx,
+                    )
+                });
+
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+    }
+
+    fn compute_target_location(
+        &self,
+        lsp_location: lsp::Location,
+        server_id: LanguageServerId,
+        cx: &mut ViewContext<Editor>,
+    ) -> Task<anyhow::Result<Option<Location>>> {
+        let Some(project) = self.project.clone() else {
+            return Task::Ready(Some(Ok(None)));
+        };
+
+        cx.spawn(move |editor, mut cx| async move {
+            let location_task = editor.update(&mut cx, |editor, cx| {
+                project.update(cx, |project, cx| {
+                    let language_server_name =
+                        editor.buffer.read(cx).as_singleton().and_then(|buffer| {
+                            project
+                                .language_server_for_buffer(buffer.read(cx), server_id, cx)
+                                .map(|(_, lsp_adapter)| {
+                                    LanguageServerName(Arc::from(lsp_adapter.name()))
+                                })
+                        });
+                    language_server_name.map(|language_server_name| {
+                        project.open_local_buffer_via_lsp(
+                            lsp_location.uri.clone(),
+                            server_id,
+                            language_server_name,
+                            cx,
+                        )
+                    })
+                })
+            })?;
+            let location = match location_task {
+                Some(task) => Some({
+                    let target_buffer_handle = task.await.context("open local buffer")?;
+                    let range = {
+                        target_buffer_handle.update(&mut cx, |target_buffer, _| {
+                            let target_start = target_buffer.clip_point_utf16(
+                                point_from_lsp(lsp_location.range.start),
+                                Bias::Left,
+                            );
+                            let target_end = target_buffer.clip_point_utf16(
+                                point_from_lsp(lsp_location.range.end),
+                                Bias::Left,
+                            );
+                            target_buffer.anchor_after(target_start)
+                                ..target_buffer.anchor_before(target_end)
+                        })
+                    };
+                    Location {
+                        buffer: target_buffer_handle,
+                        range,
+                    }
+                }),
+                None => None,
+            };
+            Ok(location)
+        })
+    }
+
+    pub fn find_all_references(
+        workspace: &mut Workspace,
+        _: &FindAllReferences,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Task<Result<()>>> {
+        let active_item = workspace.active_item(cx)?;
+        let editor_handle = active_item.act_as::<Self>(cx)?;
+
+        let editor = editor_handle.read(cx);
+        let buffer = editor.buffer.read(cx);
+        let head = editor.selections.newest::<usize>(cx).head();
+        let (buffer, head) = buffer.text_anchor_for_position(head, cx)?;
+        let replica_id = editor.replica_id(cx);
+
+        let project = workspace.project().clone();
+        let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
+        Some(cx.spawn_labeled(
+            "Finding All References...",
+            |workspace, mut cx| async move {
+                let locations = references.await?;
+                if locations.is_empty() {
+                    return Ok(());
+                }
+
+                workspace.update(&mut cx, |workspace, cx| {
+                    let title = locations
+                        .first()
+                        .as_ref()
+                        .map(|location| {
+                            let buffer = location.buffer.read(cx);
+                            format!(
+                                "References to `{}`",
+                                buffer
+                                    .text_for_range(location.range.clone())
+                                    .collect::<String>()
+                            )
+                        })
+                        .unwrap();
+                    Self::open_locations_in_multibuffer(
+                        workspace, locations, replica_id, title, false, cx,
+                    );
+                })?;
+
+                Ok(())
+            },
+        ))
+    }
+
+    /// Opens a multibuffer with the given project locations in it
+    pub fn open_locations_in_multibuffer(
+        workspace: &mut Workspace,
+        mut locations: Vec<Location>,
+        replica_id: ReplicaId,
+        title: String,
+        split: bool,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        // If there are multiple definitions, open them in a multibuffer
+        locations.sort_by_key(|location| location.buffer.read(cx).remote_id());
+        let mut locations = locations.into_iter().peekable();
+        let mut ranges_to_highlight = Vec::new();
+
+        let excerpt_buffer = cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(replica_id);
+            while let Some(location) = locations.next() {
+                let buffer = location.buffer.read(cx);
+                let mut ranges_for_buffer = Vec::new();
+                let range = location.range.to_offset(buffer);
+                ranges_for_buffer.push(range.clone());
+
+                while let Some(next_location) = locations.peek() {
+                    if next_location.buffer == location.buffer {
+                        ranges_for_buffer.push(next_location.range.to_offset(buffer));
+                        locations.next();
+                    } else {
+                        break;
+                    }
+                }
+
+                ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end)));
+                ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines(
+                    location.buffer.clone(),
+                    ranges_for_buffer,
+                    1,
+                    cx,
+                ))
+            }
+
+            multibuffer.with_title(title)
+        });
+
+        let editor = cx.add_view(|cx| {
+            Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), cx)
+        });
+        editor.update(cx, |editor, cx| {
+            editor.highlight_background::<Self>(
+                ranges_to_highlight,
+                |theme| theme.editor.highlighted_line_background,
+                cx,
+            );
+        });
+        if split {
+            workspace.split_item(SplitDirection::Right, Box::new(editor), cx);
+        } else {
+            workspace.add_item(Box::new(editor), cx);
+        }
+    }
+
+    pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+        use language::ToOffset as _;
+
+        let project = self.project.clone()?;
+        let selection = self.selections.newest_anchor().clone();
+        let (cursor_buffer, cursor_buffer_position) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(selection.head(), cx)?;
+        let (tail_buffer, _) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(selection.tail(), cx)?;
+        if tail_buffer != cursor_buffer {
+            return None;
+        }
+
+        let snapshot = cursor_buffer.read(cx).snapshot();
+        let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
+        let prepare_rename = project.update(cx, |project, cx| {
+            project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx)
+        });
+
+        Some(cx.spawn(|this, mut cx| async move {
+            let rename_range = if let Some(range) = prepare_rename.await? {
+                Some(range)
+            } else {
+                this.update(&mut cx, |this, cx| {
+                    let buffer = this.buffer.read(cx).snapshot(cx);
+                    let mut buffer_highlights = this
+                        .document_highlights_for_position(selection.head(), &buffer)
+                        .filter(|highlight| {
+                            highlight.start.excerpt_id == selection.head().excerpt_id
+                                && highlight.end.excerpt_id == selection.head().excerpt_id
+                        });
+                    buffer_highlights
+                        .next()
+                        .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor)
+                })?
+            };
+            if let Some(rename_range) = rename_range {
+                let rename_buffer_range = rename_range.to_offset(&snapshot);
+                let cursor_offset_in_rename_range =
+                    cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
+
+                this.update(&mut cx, |this, cx| {
+                    this.take_rename(false, cx);
+                    let style = this.style(cx);
+                    let buffer = this.buffer.read(cx).read(cx);
+                    let cursor_offset = selection.head().to_offset(&buffer);
+                    let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
+                    let rename_end = rename_start + rename_buffer_range.len();
+                    let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end);
+                    let mut old_highlight_id = None;
+                    let old_name: Arc<str> = buffer
+                        .chunks(rename_start..rename_end, true)
+                        .map(|chunk| {
+                            if old_highlight_id.is_none() {
+                                old_highlight_id = chunk.syntax_highlight_id;
+                            }
+                            chunk.text
+                        })
+                        .collect::<String>()
+                        .into();
+
+                    drop(buffer);
+
+                    // Position the selection in the rename editor so that it matches the current selection.
+                    this.show_local_selections = false;
+                    let rename_editor = cx.add_view(|cx| {
+                        let mut editor = Editor::single_line(None, cx);
+                        if let Some(old_highlight_id) = old_highlight_id {
+                            editor.override_text_style =
+                                Some(Box::new(move |style| old_highlight_id.style(&style.syntax)));
+                        }
+                        editor.buffer.update(cx, |buffer, cx| {
+                            buffer.edit([(0..0, old_name.clone())], None, cx)
+                        });
+                        editor.select_all(&SelectAll, cx);
+                        editor
+                    });
+
+                    let ranges = this
+                        .clear_background_highlights::<DocumentHighlightWrite>(cx)
+                        .into_iter()
+                        .flat_map(|(_, ranges)| ranges.into_iter())
+                        .chain(
+                            this.clear_background_highlights::<DocumentHighlightRead>(cx)
+                                .into_iter()
+                                .flat_map(|(_, ranges)| ranges.into_iter()),
+                        )
+                        .collect();
+
+                    this.highlight_text::<Rename>(
+                        ranges,
+                        HighlightStyle {
+                            fade_out: Some(style.rename_fade),
+                            ..Default::default()
+                        },
+                        cx,
+                    );
+                    cx.focus(&rename_editor);
+                    let block_id = this.insert_blocks(
+                        [BlockProperties {
+                            style: BlockStyle::Flex,
+                            position: range.start.clone(),
+                            height: 1,
+                            render: Arc::new({
+                                let editor = rename_editor.clone();
+                                move |cx: &mut BlockContext| {
+                                    ChildView::new(&editor, cx)
+                                        .contained()
+                                        .with_padding_left(cx.anchor_x)
+                                        .into_any()
+                                }
+                            }),
+                            disposition: BlockDisposition::Below,
+                        }],
+                        Some(Autoscroll::fit()),
+                        cx,
+                    )[0];
+                    this.pending_rename = Some(RenameState {
+                        range,
+                        old_name,
+                        editor: rename_editor,
+                        block_id,
+                    });
+                })?;
+            }
+
+            Ok(())
+        }))
+    }
+
+    pub fn confirm_rename(
+        workspace: &mut Workspace,
+        _: &ConfirmRename,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Task<Result<()>>> {
+        let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
+
+        let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| {
+            let rename = editor.take_rename(false, cx)?;
+            let buffer = editor.buffer.read(cx);
+            let (start_buffer, start) =
+                buffer.text_anchor_for_position(rename.range.start.clone(), cx)?;
+            let (end_buffer, end) =
+                buffer.text_anchor_for_position(rename.range.end.clone(), cx)?;
+            if start_buffer == end_buffer {
+                let new_name = rename.editor.read(cx).text(cx);
+                Some((start_buffer, start..end, rename.old_name, new_name))
+            } else {
+                None
+            }
+        })?;
+
+        let rename = workspace.project().clone().update(cx, |project, cx| {
+            project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx)
+        });
+
+        let editor = editor.downgrade();
+        Some(cx.spawn(|workspace, mut cx| async move {
+            let project_transaction = rename.await?;
+            Self::open_project_transaction(
+                &editor,
+                workspace,
+                project_transaction,
+                format!("Rename: {} โ†’ {}", old_name, new_name),
+                cx.clone(),
+            )
+            .await?;
+
+            editor.update(&mut cx, |editor, cx| {
+                editor.refresh_document_highlights(cx);
+            })?;
+            Ok(())
+        }))
+    }
+
+    fn take_rename(
+        &mut self,
+        moving_cursor: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<RenameState> {
+        let rename = self.pending_rename.take()?;
+        self.remove_blocks(
+            [rename.block_id].into_iter().collect(),
+            Some(Autoscroll::fit()),
+            cx,
+        );
+        self.clear_highlights::<Rename>(cx);
+        self.show_local_selections = true;
+
+        if moving_cursor {
+            let rename_editor = rename.editor.read(cx);
+            let cursor_in_rename_editor = rename_editor.selections.newest::<usize>(cx).head();
+
+            // Update the selection to match the position of the selection inside
+            // the rename editor.
+            let snapshot = self.buffer.read(cx).read(cx);
+            let rename_range = rename.range.to_offset(&snapshot);
+            let cursor_in_editor = snapshot
+                .clip_offset(rename_range.start + cursor_in_rename_editor, Bias::Left)
+                .min(rename_range.end);
+            drop(snapshot);
+
+            self.change_selections(None, cx, |s| {
+                s.select_ranges(vec![cursor_in_editor..cursor_in_editor])
+            });
+        } else {
+            self.refresh_document_highlights(cx);
+        }
+
+        Some(rename)
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn pending_rename(&self) -> Option<&RenameState> {
+        self.pending_rename.as_ref()
+    }
+
+    fn format(&mut self, _: &Format, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+        let project = match &self.project {
+            Some(project) => project.clone(),
+            None => return None,
+        };
+
+        Some(self.perform_format(project, FormatTrigger::Manual, cx))
+    }
+
+    fn perform_format(
+        &mut self,
+        project: ModelHandle<Project>,
+        trigger: FormatTrigger,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let buffer = self.buffer().clone();
+        let buffers = buffer.read(cx).all_buffers();
+
+        let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
+        let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
+
+        cx.spawn(|_, mut cx| async move {
+            let transaction = futures::select_biased! {
+                _ = timeout => {
+                    log::warn!("timed out waiting for formatting");
+                    None
+                }
+                transaction = format.log_err().fuse() => transaction,
+            };
+
+            buffer.update(&mut cx, |buffer, cx| {
+                if let Some(transaction) = transaction {
+                    if !buffer.is_singleton() {
+                        buffer.push_transaction(&transaction.0, cx);
+                    }
+                }
+
+                cx.notify();
+            });
+
+            Ok(())
+        })
+    }
+
+    fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext<Self>) {
+        if let Some(project) = self.project.clone() {
+            self.buffer.update(cx, |multi_buffer, cx| {
+                project.update(cx, |project, cx| {
+                    project.restart_language_servers_for_buffers(multi_buffer.all_buffers(), cx);
+                });
+            })
+        }
+    }
+
+    fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
+        cx.show_character_palette();
+    }
+
+    fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext<Editor>) {
+        if let Some(active_diagnostics) = self.active_diagnostics.as_mut() {
+            let buffer = self.buffer.read(cx).snapshot(cx);
+            let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer);
+            let is_valid = buffer
+                .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone(), false)
+                .any(|entry| {
+                    entry.diagnostic.is_primary
+                        && !entry.range.is_empty()
+                        && entry.range.start == primary_range_start
+                        && entry.diagnostic.message == active_diagnostics.primary_message
+                });
+
+            if is_valid != active_diagnostics.is_valid {
+                active_diagnostics.is_valid = is_valid;
+                let mut new_styles = HashMap::default();
+                for (block_id, diagnostic) in &active_diagnostics.blocks {
+                    new_styles.insert(
+                        *block_id,
+                        diagnostic_block_renderer(diagnostic.clone(), is_valid),
+                    );
+                }
+                self.display_map
+                    .update(cx, |display_map, _| display_map.replace_blocks(new_styles));
+            }
+        }
+    }
+
+    fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext<Self>) -> bool {
+        self.dismiss_diagnostics(cx);
+        self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
+            let buffer = self.buffer.read(cx).snapshot(cx);
+
+            let mut primary_range = None;
+            let mut primary_message = None;
+            let mut group_end = Point::zero();
+            let diagnostic_group = buffer
+                .diagnostic_group::<Point>(group_id)
+                .map(|entry| {
+                    if entry.range.end > group_end {
+                        group_end = entry.range.end;
+                    }
+                    if entry.diagnostic.is_primary {
+                        primary_range = Some(entry.range.clone());
+                        primary_message = Some(entry.diagnostic.message.clone());
+                    }
+                    entry
+                })
+                .collect::<Vec<_>>();
+            let primary_range = primary_range?;
+            let primary_message = primary_message?;
+            let primary_range =
+                buffer.anchor_after(primary_range.start)..buffer.anchor_before(primary_range.end);
+
+            let blocks = display_map
+                .insert_blocks(
+                    diagnostic_group.iter().map(|entry| {
+                        let diagnostic = entry.diagnostic.clone();
+                        let message_height = diagnostic.message.lines().count() as u8;
+                        BlockProperties {
+                            style: BlockStyle::Fixed,
+                            position: buffer.anchor_after(entry.range.start),
+                            height: message_height,
+                            render: diagnostic_block_renderer(diagnostic, true),
+                            disposition: BlockDisposition::Below,
+                        }
+                    }),
+                    cx,
+                )
+                .into_iter()
+                .zip(diagnostic_group.into_iter().map(|entry| entry.diagnostic))
+                .collect();
+
+            Some(ActiveDiagnosticGroup {
+                primary_range,
+                primary_message,
+                blocks,
+                is_valid: true,
+            })
+        });
+        self.active_diagnostics.is_some()
+    }
+
+    fn dismiss_diagnostics(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(active_diagnostic_group) = self.active_diagnostics.take() {
+            self.display_map.update(cx, |display_map, cx| {
+                display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx);
+            });
+            cx.notify();
+        }
+    }
+
+    pub fn set_selections_from_remote(
+        &mut self,
+        selections: Vec<Selection<Anchor>>,
+        pending_selection: Option<Selection<Anchor>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let old_cursor_position = self.selections.newest_anchor().head();
+        self.selections.change_with(cx, |s| {
+            s.select_anchors(selections);
+            if let Some(pending_selection) = pending_selection {
+                s.set_pending(pending_selection, SelectMode::Character);
+            } else {
+                s.clear_pending();
+            }
+        });
+        self.selections_did_change(false, &old_cursor_position, cx);
+    }
+
+    fn push_to_selection_history(&mut self) {
+        self.selection_history.push(SelectionHistoryEntry {
+            selections: self.selections.disjoint_anchors(),
+            select_next_state: self.select_next_state.clone(),
+            select_prev_state: self.select_prev_state.clone(),
+            add_selections_state: self.add_selections_state.clone(),
+        });
+    }
+
+    pub fn transact(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+        update: impl FnOnce(&mut Self, &mut ViewContext<Self>),
+    ) -> Option<TransactionId> {
+        self.start_transaction_at(Instant::now(), cx);
+        update(self, cx);
+        self.end_transaction_at(Instant::now(), cx)
+    }
+
+    fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext<Self>) {
+        self.end_selection(cx);
+        if let Some(tx_id) = self
+            .buffer
+            .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx))
+        {
+            self.selection_history
+                .insert_transaction(tx_id, self.selections.disjoint_anchors());
+        }
+    }
+
+    fn end_transaction_at(
+        &mut self,
+        now: Instant,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<TransactionId> {
+        if let Some(tx_id) = self
+            .buffer
+            .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
+        {
+            if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) {
+                *end_selections = Some(self.selections.disjoint_anchors());
+            } else {
+                error!("unexpectedly ended a transaction that wasn't started by this editor");
+            }
+
+            cx.emit(Event::Edited);
+            Some(tx_id)
+        } else {
+            None
+        }
+    }
+
+    pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext<Self>) {
+        let mut fold_ranges = Vec::new();
+
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        let selections = self.selections.all_adjusted(cx);
+        for selection in selections {
+            let range = selection.range().sorted();
+            let buffer_start_row = range.start.row;
+
+            for row in (0..=range.end.row).rev() {
+                let fold_range = display_map.foldable_range(row);
+
+                if let Some(fold_range) = fold_range {
+                    if fold_range.end.row >= buffer_start_row {
+                        fold_ranges.push(fold_range);
+                        if row <= range.start.row {
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        self.fold_ranges(fold_ranges, true, cx);
+    }
+
+    pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext<Self>) {
+        let buffer_row = fold_at.buffer_row;
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        if let Some(fold_range) = display_map.foldable_range(buffer_row) {
+            let autoscroll = self
+                .selections
+                .all::<Point>(cx)
+                .iter()
+                .any(|selection| fold_range.overlaps(&selection.range()));
+
+            self.fold_ranges(std::iter::once(fold_range), autoscroll, cx);
+        }
+    }
+
+    pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = &display_map.buffer_snapshot;
+        let selections = self.selections.all::<Point>(cx);
+        let ranges = selections
+            .iter()
+            .map(|s| {
+                let range = s.display_range(&display_map).sorted();
+                let mut start = range.start.to_point(&display_map);
+                let mut end = range.end.to_point(&display_map);
+                start.column = 0;
+                end.column = buffer.line_len(end.row);
+                start..end
+            })
+            .collect::<Vec<_>>();
+
+        self.unfold_ranges(ranges, true, true, cx);
+    }
+
+    pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        let intersection_range = Point::new(unfold_at.buffer_row, 0)
+            ..Point::new(
+                unfold_at.buffer_row,
+                display_map.buffer_snapshot.line_len(unfold_at.buffer_row),
+            );
+
+        let autoscroll = self
+            .selections
+            .all::<Point>(cx)
+            .iter()
+            .any(|selection| selection.range().overlaps(&intersection_range));
+
+        self.unfold_ranges(std::iter::once(intersection_range), true, autoscroll, cx)
+    }
+
+    pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
+        let selections = self.selections.all::<Point>(cx);
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let line_mode = self.selections.line_mode;
+        let ranges = selections.into_iter().map(|s| {
+            if line_mode {
+                let start = Point::new(s.start.row, 0);
+                let end = Point::new(s.end.row, display_map.buffer_snapshot.line_len(s.end.row));
+                start..end
+            } else {
+                s.start..s.end
+            }
+        });
+        self.fold_ranges(ranges, true, cx);
+    }
+
+    pub fn fold_ranges<T: ToOffset + Clone>(
+        &mut self,
+        ranges: impl IntoIterator<Item = Range<T>>,
+        auto_scroll: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let mut ranges = ranges.into_iter().peekable();
+        if ranges.peek().is_some() {
+            self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
+
+            if auto_scroll {
+                self.request_autoscroll(Autoscroll::fit(), cx);
+            }
+
+            cx.notify();
+        }
+    }
+
+    pub fn unfold_ranges<T: ToOffset + Clone>(
+        &mut self,
+        ranges: impl IntoIterator<Item = Range<T>>,
+        inclusive: bool,
+        auto_scroll: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let mut ranges = ranges.into_iter().peekable();
+        if ranges.peek().is_some() {
+            self.display_map
+                .update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
+            if auto_scroll {
+                self.request_autoscroll(Autoscroll::fit(), cx);
+            }
+
+            cx.notify();
+        }
+    }
+
+    pub fn gutter_hover(
+        &mut self,
+        GutterHover { hovered }: &GutterHover,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.gutter_hovered = *hovered;
+        cx.notify();
+    }
+
+    pub fn insert_blocks(
+        &mut self,
+        blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
+        autoscroll: Option<Autoscroll>,
+        cx: &mut ViewContext<Self>,
+    ) -> Vec<BlockId> {
+        let blocks = self
+            .display_map
+            .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
+        if let Some(autoscroll) = autoscroll {
+            self.request_autoscroll(autoscroll, cx);
+        }
+        blocks
+    }
+
+    pub fn replace_blocks(
+        &mut self,
+        blocks: HashMap<BlockId, RenderBlock>,
+        autoscroll: Option<Autoscroll>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.display_map
+            .update(cx, |display_map, _| display_map.replace_blocks(blocks));
+        if let Some(autoscroll) = autoscroll {
+            self.request_autoscroll(autoscroll, cx);
+        }
+    }
+
+    pub fn remove_blocks(
+        &mut self,
+        block_ids: HashSet<BlockId>,
+        autoscroll: Option<Autoscroll>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.display_map.update(cx, |display_map, cx| {
+            display_map.remove_blocks(block_ids, cx)
+        });
+        if let Some(autoscroll) = autoscroll {
+            self.request_autoscroll(autoscroll, cx);
+        }
+    }
+
+    pub fn longest_row(&self, cx: &mut AppContext) -> u32 {
+        self.display_map
+            .update(cx, |map, cx| map.snapshot(cx))
+            .longest_row()
+    }
+
+    pub fn max_point(&self, cx: &mut AppContext) -> DisplayPoint {
+        self.display_map
+            .update(cx, |map, cx| map.snapshot(cx))
+            .max_point()
+    }
+
+    pub fn text(&self, cx: &AppContext) -> String {
+        self.buffer.read(cx).read(cx).text()
+    }
+
+    pub fn set_text(&mut self, text: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
+        self.transact(cx, |this, cx| {
+            this.buffer
+                .read(cx)
+                .as_singleton()
+                .expect("you can only call set_text on editors for singleton buffers")
+                .update(cx, |buffer, cx| buffer.set_text(text, cx));
+        });
+    }
+
+    pub fn display_text(&self, cx: &mut AppContext) -> String {
+        self.display_map
+            .update(cx, |map, cx| map.snapshot(cx))
+            .text()
+    }
+
+    pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> {
+        let mut wrap_guides = smallvec::smallvec![];
+
+        if self.show_wrap_guides == Some(false) {
+            return wrap_guides;
+        }
+
+        let settings = self.buffer.read(cx).settings_at(0, cx);
+        if settings.show_wrap_guides {
+            if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) {
+                wrap_guides.push((soft_wrap as usize, true));
+            }
+            wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false)))
+        }
+
+        wrap_guides
+    }
+
+    pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
+        let settings = self.buffer.read(cx).settings_at(0, cx);
+        let mode = self
+            .soft_wrap_mode_override
+            .unwrap_or_else(|| settings.soft_wrap);
+        match mode {
+            language_settings::SoftWrap::None => SoftWrap::None,
+            language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
+            language_settings::SoftWrap::PreferredLineLength => {
+                SoftWrap::Column(settings.preferred_line_length)
+            }
+        }
+    }
+
+    pub fn set_soft_wrap_mode(
+        &mut self,
+        mode: language_settings::SoftWrap,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.soft_wrap_mode_override = Some(mode);
+        cx.notify();
+    }
+
+    pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut AppContext) -> bool {
+        self.display_map
+            .update(cx, |map, cx| map.set_wrap_width(width, cx))
+    }
+
+    pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext<Self>) {
+        if self.soft_wrap_mode_override.is_some() {
+            self.soft_wrap_mode_override.take();
+        } else {
+            let soft_wrap = match self.soft_wrap_mode(cx) {
+                SoftWrap::None => language_settings::SoftWrap::EditorWidth,
+                SoftWrap::EditorWidth | SoftWrap::Column(_) => language_settings::SoftWrap::None,
+            };
+            self.soft_wrap_mode_override = Some(soft_wrap);
+        }
+        cx.notify();
+    }
+
+    pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
+        self.show_gutter = show_gutter;
+        cx.notify();
+    }
+
+    pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
+        self.show_wrap_guides = Some(show_gutter);
+        cx.notify();
+    }
+
+    pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
+        if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+            if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
+                cx.reveal_path(&file.abs_path(cx));
+            }
+        }
+    }
+
+    pub fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
+        if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+            if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
+                if let Some(path) = file.abs_path(cx).to_str() {
+                    cx.write_to_clipboard(ClipboardItem::new(path.to_string()));
+                }
+            }
+        }
+    }
+
+    pub fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
+        if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+            if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
+                if let Some(path) = file.path().to_str() {
+                    cx.write_to_clipboard(ClipboardItem::new(path.to_string()));
+                }
+            }
+        }
+    }
+
+    pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
+        self.highlighted_rows = rows;
+    }
+
+    pub fn highlighted_rows(&self) -> Option<Range<u32>> {
+        self.highlighted_rows.clone()
+    }
+
+    pub fn highlight_background<T: 'static>(
+        &mut self,
+        ranges: Vec<Range<Anchor>>,
+        color_fetcher: fn(&Theme) -> Color,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.background_highlights
+            .insert(TypeId::of::<T>(), (color_fetcher, ranges));
+        cx.notify();
+    }
+
+    pub fn highlight_inlay_background<T: 'static>(
+        &mut self,
+        ranges: Vec<InlayHighlight>,
+        color_fetcher: fn(&Theme) -> Color,
+        cx: &mut ViewContext<Self>,
+    ) {
+        // TODO: no actual highlights happen for inlays currently, find a way to do that
+        self.inlay_background_highlights
+            .insert(Some(TypeId::of::<T>()), (color_fetcher, ranges));
+        cx.notify();
+    }
+
+    pub fn clear_background_highlights<T: 'static>(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<BackgroundHighlight> {
+        let text_highlights = self.background_highlights.remove(&TypeId::of::<T>());
+        let inlay_highlights = self
+            .inlay_background_highlights
+            .remove(&Some(TypeId::of::<T>()));
+        if text_highlights.is_some() || inlay_highlights.is_some() {
+            cx.notify();
+        }
+        text_highlights
+    }
+
+    #[cfg(feature = "test-support")]
+    pub fn all_text_background_highlights(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+    ) -> Vec<(Range<DisplayPoint>, Color)> {
+        let snapshot = self.snapshot(cx);
+        let buffer = &snapshot.buffer_snapshot;
+        let start = buffer.anchor_before(0);
+        let end = buffer.anchor_after(buffer.len());
+        let theme = theme::current(cx);
+        self.background_highlights_in_range(start..end, &snapshot, theme.as_ref())
+    }
+
+    fn document_highlights_for_position<'a>(
+        &'a self,
+        position: Anchor,
+        buffer: &'a MultiBufferSnapshot,
+    ) -> impl 'a + Iterator<Item = &Range<Anchor>> {
+        let read_highlights = self
+            .background_highlights
+            .get(&TypeId::of::<DocumentHighlightRead>())
+            .map(|h| &h.1);
+        let write_highlights = self
+            .background_highlights
+            .get(&TypeId::of::<DocumentHighlightWrite>())
+            .map(|h| &h.1);
+        let left_position = position.bias_left(buffer);
+        let right_position = position.bias_right(buffer);
+        read_highlights
+            .into_iter()
+            .chain(write_highlights)
+            .flat_map(move |ranges| {
+                let start_ix = match ranges.binary_search_by(|probe| {
+                    let cmp = probe.end.cmp(&left_position, buffer);
+                    if cmp.is_ge() {
+                        Ordering::Greater
+                    } else {
+                        Ordering::Less
+                    }
+                }) {
+                    Ok(i) | Err(i) => i,
+                };
+
+                let right_position = right_position.clone();
+                ranges[start_ix..]
+                    .iter()
+                    .take_while(move |range| range.start.cmp(&right_position, buffer).is_le())
+            })
+    }
+
+    pub fn background_highlights_in_range(
+        &self,
+        search_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+        theme: &Theme,
+    ) -> Vec<(Range<DisplayPoint>, Color)> {
+        let mut results = Vec::new();
+        for (color_fetcher, ranges) in self.background_highlights.values() {
+            let color = color_fetcher(theme);
+            let start_ix = match ranges.binary_search_by(|probe| {
+                let cmp = probe
+                    .end
+                    .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
+                if cmp.is_gt() {
+                    Ordering::Greater
+                } else {
+                    Ordering::Less
+                }
+            }) {
+                Ok(i) | Err(i) => i,
+            };
+            for range in &ranges[start_ix..] {
+                if range
+                    .start
+                    .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
+                    .is_ge()
+                {
+                    break;
+                }
+
+                let start = range.start.to_display_point(&display_snapshot);
+                let end = range.end.to_display_point(&display_snapshot);
+                results.push((start..end, color))
+            }
+        }
+        results
+    }
+
+    pub fn background_highlight_row_ranges<T: 'static>(
+        &self,
+        search_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+        count: usize,
+    ) -> Vec<RangeInclusive<DisplayPoint>> {
+        let mut results = Vec::new();
+        let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::<T>()) else {
+            return vec![];
+        };
+
+        let start_ix = match ranges.binary_search_by(|probe| {
+            let cmp = probe
+                .end
+                .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
+            if cmp.is_gt() {
+                Ordering::Greater
+            } else {
+                Ordering::Less
+            }
+        }) {
+            Ok(i) | Err(i) => i,
+        };
+        let mut push_region = |start: Option<Point>, end: Option<Point>| {
+            if let (Some(start_display), Some(end_display)) = (start, end) {
+                results.push(
+                    start_display.to_display_point(display_snapshot)
+                        ..=end_display.to_display_point(display_snapshot),
+                );
+            }
+        };
+        let mut start_row: Option<Point> = None;
+        let mut end_row: Option<Point> = None;
+        if ranges.len() > count {
+            return Vec::new();
+        }
+        for range in &ranges[start_ix..] {
+            if range
+                .start
+                .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
+                .is_ge()
+            {
+                break;
+            }
+            let end = range.end.to_point(&display_snapshot.buffer_snapshot);
+            if let Some(current_row) = &end_row {
+                if end.row == current_row.row {
+                    continue;
+                }
+            }
+            let start = range.start.to_point(&display_snapshot.buffer_snapshot);
+            if start_row.is_none() {
+                assert_eq!(end_row, None);
+                start_row = Some(start);
+                end_row = Some(end);
+                continue;
+            }
+            if let Some(current_end) = end_row.as_mut() {
+                if start.row > current_end.row + 1 {
+                    push_region(start_row, end_row);
+                    start_row = Some(start);
+                    end_row = Some(end);
+                } else {
+                    // Merge two hunks.
+                    *current_end = end;
+                }
+            } else {
+                unreachable!();
+            }
+        }
+        // We might still have a hunk that was not rendered (if there was a search hit on the last line)
+        push_region(start_row, end_row);
+        results
+    }
+
+    pub fn highlight_text<T: 'static>(
+        &mut self,
+        ranges: Vec<Range<Anchor>>,
+        style: HighlightStyle,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.display_map.update(cx, |map, _| {
+            map.highlight_text(TypeId::of::<T>(), ranges, style)
+        });
+        cx.notify();
+    }
+
+    pub fn highlight_inlays<T: 'static>(
+        &mut self,
+        highlights: Vec<InlayHighlight>,
+        style: HighlightStyle,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.display_map.update(cx, |map, _| {
+            map.highlight_inlays(TypeId::of::<T>(), highlights, style)
+        });
+        cx.notify();
+    }
+
+    pub fn text_highlights<'a, T: 'static>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> Option<(HighlightStyle, &'a [Range<Anchor>])> {
+        self.display_map.read(cx).text_highlights(TypeId::of::<T>())
+    }
+
+    pub fn clear_highlights<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
+        let cleared = self
+            .display_map
+            .update(cx, |map, _| map.clear_highlights(TypeId::of::<T>()));
+        if cleared {
+            cx.notify();
+        }
+    }
+
+    pub fn show_local_cursors(&self, cx: &AppContext) -> bool {
+        self.blink_manager.read(cx).visible() && self.focused
+    }
+
+    fn on_buffer_changed(&mut self, _: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Self>) {
+        cx.notify();
+    }
+
+    fn on_buffer_event(
+        &mut self,
+        multibuffer: ModelHandle<MultiBuffer>,
+        event: &multi_buffer::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            multi_buffer::Event::Edited {
+                sigleton_buffer_edited,
+            } => {
+                self.refresh_active_diagnostics(cx);
+                self.refresh_code_actions(cx);
+                if self.has_active_copilot_suggestion(cx) {
+                    self.update_visible_copilot_suggestion(cx);
+                }
+                cx.emit(Event::BufferEdited);
+
+                if *sigleton_buffer_edited {
+                    if let Some(project) = &self.project {
+                        let project = project.read(cx);
+                        let languages_affected = multibuffer
+                            .read(cx)
+                            .all_buffers()
+                            .into_iter()
+                            .filter_map(|buffer| {
+                                let buffer = buffer.read(cx);
+                                let language = buffer.language()?;
+                                if project.is_local()
+                                    && project.language_servers_for_buffer(buffer, cx).count() == 0
+                                {
+                                    None
+                                } else {
+                                    Some(language)
+                                }
+                            })
+                            .cloned()
+                            .collect::<HashSet<_>>();
+                        if !languages_affected.is_empty() {
+                            self.refresh_inlay_hints(
+                                InlayHintRefreshReason::BufferEdited(languages_affected),
+                                cx,
+                            );
+                        }
+                    }
+                }
+            }
+            multi_buffer::Event::ExcerptsAdded {
+                buffer,
+                predecessor,
+                excerpts,
+            } => {
+                cx.emit(Event::ExcerptsAdded {
+                    buffer: buffer.clone(),
+                    predecessor: *predecessor,
+                    excerpts: excerpts.clone(),
+                });
+                self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+            }
+            multi_buffer::Event::ExcerptsRemoved { ids } => {
+                self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
+                cx.emit(Event::ExcerptsRemoved { ids: ids.clone() })
+            }
+            multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed),
+            multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
+            multi_buffer::Event::Saved => cx.emit(Event::Saved),
+            multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
+            multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged),
+            multi_buffer::Event::DiffBaseChanged => cx.emit(Event::DiffBaseChanged),
+            multi_buffer::Event::Closed => cx.emit(Event::Closed),
+            multi_buffer::Event::DiagnosticsUpdated => {
+                self.refresh_active_diagnostics(cx);
+            }
+            _ => {}
+        };
+    }
+
+    fn on_display_map_changed(&mut self, _: ModelHandle<DisplayMap>, cx: &mut ViewContext<Self>) {
+        cx.notify();
+    }
+
+    fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
+        self.refresh_copilot_suggestions(true, cx);
+        self.refresh_inlay_hints(
+            InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
+                self.selections.newest_anchor().head(),
+                &self.buffer.read(cx).snapshot(cx),
+                cx,
+            )),
+            cx,
+        );
+    }
+
+    pub fn set_searchable(&mut self, searchable: bool) {
+        self.searchable = searchable;
+    }
+
+    pub fn searchable(&self) -> bool {
+        self.searchable
+    }
+
+    fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext<Workspace>) {
+        let active_item = workspace.active_item(cx);
+        let editor_handle = if let Some(editor) = active_item
+            .as_ref()
+            .and_then(|item| item.act_as::<Self>(cx))
+        {
+            editor
+        } else {
+            cx.propagate_action();
+            return;
+        };
+
+        let editor = editor_handle.read(cx);
+        let buffer = editor.buffer.read(cx);
+        if buffer.is_singleton() {
+            cx.propagate_action();
+            return;
+        }
+
+        let mut new_selections_by_buffer = HashMap::default();
+        for selection in editor.selections.all::<usize>(cx) {
+            for (buffer, mut range, _) in
+                buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
+            {
+                if selection.reversed {
+                    mem::swap(&mut range.start, &mut range.end);
+                }
+                new_selections_by_buffer
+                    .entry(buffer)
+                    .or_insert(Vec::new())
+                    .push(range)
+            }
+        }
+
+        editor_handle.update(cx, |editor, cx| {
+            editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
+        });
+        let pane = workspace.active_pane().clone();
+        pane.update(cx, |pane, _| pane.disable_history());
+
+        // We defer the pane interaction because we ourselves are a workspace item
+        // and activating a new item causes the pane to call a method on us reentrantly,
+        // which panics if we're on the stack.
+        cx.defer(move |workspace, cx| {
+            for (buffer, ranges) in new_selections_by_buffer.into_iter() {
+                let editor = workspace.open_project_item::<Self>(buffer, cx);
+                editor.update(cx, |editor, cx| {
+                    editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
+                        s.select_ranges(ranges);
+                    });
+                });
+            }
+
+            pane.update(cx, |pane, _| pane.enable_history());
+        });
+    }
+
+    fn jump(
+        workspace: &mut Workspace,
+        path: ProjectPath,
+        position: Point,
+        anchor: language::Anchor,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let editor = workspace.open_path(path, None, true, cx);
+        cx.spawn(|_, mut cx| async move {
+            let editor = editor
+                .await?
+                .downcast::<Editor>()
+                .ok_or_else(|| anyhow!("opened item was not an editor"))?
+                .downgrade();
+            editor.update(&mut cx, |editor, cx| {
+                let buffer = editor
+                    .buffer()
+                    .read(cx)
+                    .as_singleton()
+                    .ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?;
+                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;
+
+                anyhow::Ok(())
+            })??;
+
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    fn marked_text_ranges(&self, cx: &AppContext) -> Option<Vec<Range<OffsetUtf16>>> {
+        let snapshot = self.buffer.read(cx).read(cx);
+        let (_, ranges) = self.text_highlights::<InputComposition>(cx)?;
+        Some(
+            ranges
+                .iter()
+                .map(move |range| {
+                    range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot)
+                })
+                .collect(),
+        )
+    }
+
+    fn selection_replacement_ranges(
+        &self,
+        range: Range<OffsetUtf16>,
+        cx: &AppContext,
+    ) -> Vec<Range<OffsetUtf16>> {
+        let selections = self.selections.all::<OffsetUtf16>(cx);
+        let newest_selection = selections
+            .iter()
+            .max_by_key(|selection| selection.id)
+            .unwrap();
+        let start_delta = range.start.0 as isize - newest_selection.start.0 as isize;
+        let end_delta = range.end.0 as isize - newest_selection.end.0 as isize;
+        let snapshot = self.buffer.read(cx).read(cx);
+        selections
+            .into_iter()
+            .map(|mut selection| {
+                selection.start.0 =
+                    (selection.start.0 as isize).saturating_add(start_delta) as usize;
+                selection.end.0 = (selection.end.0 as isize).saturating_add(end_delta) as usize;
+                snapshot.clip_offset_utf16(selection.start, Bias::Left)
+                    ..snapshot.clip_offset_utf16(selection.end, Bias::Right)
+            })
+            .collect()
+    }
+
+    fn report_copilot_event(
+        &self,
+        suggestion_id: Option<String>,
+        suggestion_accepted: bool,
+        cx: &AppContext,
+    ) {
+        let Some(project) = &self.project else { return };
+
+        // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension
+        let file_extension = self
+            .buffer
+            .read(cx)
+            .as_singleton()
+            .and_then(|b| b.read(cx).file())
+            .and_then(|file| Path::new(file.file_name(cx)).extension())
+            .and_then(|e| e.to_str())
+            .map(|a| a.to_string());
+
+        let telemetry = project.read(cx).client().telemetry().clone();
+        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+
+        let event = ClickhouseEvent::Copilot {
+            suggestion_id,
+            suggestion_accepted,
+            file_extension,
+        };
+        telemetry.report_clickhouse_event(event, telemetry_settings);
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    fn report_editor_event(
+        &self,
+        _operation: &'static str,
+        _file_extension: Option<String>,
+        _cx: &AppContext,
+    ) {
+    }
+
+    #[cfg(not(any(test, feature = "test-support")))]
+    fn report_editor_event(
+        &self,
+        operation: &'static str,
+        file_extension: Option<String>,
+        cx: &AppContext,
+    ) {
+        let Some(project) = &self.project else { return };
+
+        // If None, we are in a file without an extension
+        let file = self
+            .buffer
+            .read(cx)
+            .as_singleton()
+            .and_then(|b| b.read(cx).file());
+        let file_extension = file_extension.or(file
+            .as_ref()
+            .and_then(|file| Path::new(file.file_name(cx)).extension())
+            .and_then(|e| e.to_str())
+            .map(|a| a.to_string()));
+
+        let vim_mode = cx
+            .global::<SettingsStore>()
+            .raw_user_settings()
+            .get("vim_mode")
+            == Some(&serde_json::Value::Bool(true));
+        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+        let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None);
+        let copilot_enabled_for_language = self
+            .buffer
+            .read(cx)
+            .settings_at(0, cx)
+            .show_copilot_suggestions;
+
+        let telemetry = project.read(cx).client().telemetry().clone();
+        let event = ClickhouseEvent::Editor {
+            file_extension,
+            vim_mode,
+            operation,
+            copilot_enabled,
+            copilot_enabled_for_language,
+        };
+        telemetry.report_clickhouse_event(event, telemetry_settings)
+    }
+
+    /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
+    /// with each line being an array of {text, highlight} objects.
+    fn copy_highlight_json(&mut self, _: &CopyHighlightJson, cx: &mut ViewContext<Self>) {
+        let Some(buffer) = self.buffer.read(cx).as_singleton() else {
+            return;
+        };
+
+        #[derive(Serialize)]
+        struct Chunk<'a> {
+            text: String,
+            highlight: Option<&'a str>,
+        }
+
+        let snapshot = buffer.read(cx).snapshot();
+        let range = self
+            .selected_text_range(cx)
+            .and_then(|selected_range| {
+                if selected_range.is_empty() {
+                    None
+                } else {
+                    Some(selected_range)
+                }
+            })
+            .unwrap_or_else(|| 0..snapshot.len());
+
+        let chunks = snapshot.chunks(range, true);
+        let mut lines = Vec::new();
+        let mut line: VecDeque<Chunk> = VecDeque::new();
+
+        let theme = &theme::current(cx).editor.syntax;
+
+        for chunk in chunks {
+            let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme));
+            let mut chunk_lines = chunk.text.split("\n").peekable();
+            while let Some(text) = chunk_lines.next() {
+                let mut merged_with_last_token = false;
+                if let Some(last_token) = line.back_mut() {
+                    if last_token.highlight == highlight {
+                        last_token.text.push_str(text);
+                        merged_with_last_token = true;
+                    }
+                }
+
+                if !merged_with_last_token {
+                    line.push_back(Chunk {
+                        text: text.into(),
+                        highlight,
+                    });
+                }
+
+                if chunk_lines.peek().is_some() {
+                    if line.len() > 1 && line.front().unwrap().text.is_empty() {
+                        line.pop_front();
+                    }
+                    if line.len() > 1 && line.back().unwrap().text.is_empty() {
+                        line.pop_back();
+                    }
+
+                    lines.push(mem::take(&mut line));
+                }
+            }
+        }
+
+        let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else {
+            return;
+        };
+        cx.write_to_clipboard(ClipboardItem::new(lines));
+    }
+
+    pub fn inlay_hint_cache(&self) -> &InlayHintCache {
+        &self.inlay_hint_cache
+    }
+
+    pub fn replay_insert_event(
+        &mut self,
+        text: &str,
+        relative_utf16_range: Option<Range<isize>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if !self.input_enabled {
+            cx.emit(Event::InputIgnored { text: text.into() });
+            return;
+        }
+        if let Some(relative_utf16_range) = relative_utf16_range {
+            let selections = self.selections.all::<OffsetUtf16>(cx);
+            self.change_selections(None, cx, |s| {
+                let new_ranges = selections.into_iter().map(|range| {
+                    let start = OffsetUtf16(
+                        range
+                            .head()
+                            .0
+                            .saturating_add_signed(relative_utf16_range.start),
+                    );
+                    let end = OffsetUtf16(
+                        range
+                            .head()
+                            .0
+                            .saturating_add_signed(relative_utf16_range.end),
+                    );
+                    start..end
+                });
+                s.select_ranges(new_ranges);
+            });
+        }
+
+        self.handle_input(text, cx);
+    }
+
+    pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool {
+        let Some(project) = self.project.as_ref() else {
+            return false;
+        };
+        let project = project.read(cx);
+
+        let mut supports = false;
+        self.buffer().read(cx).for_each_buffer(|buffer| {
+            if !supports {
+                supports = project
+                    .language_servers_for_buffer(buffer.read(cx), cx)
+                    .any(
+                        |(_, server)| match server.capabilities().inlay_hint_provider {
+                            Some(lsp::OneOf::Left(enabled)) => enabled,
+                            Some(lsp::OneOf::Right(_)) => true,
+                            None => false,
+                        },
+                    )
+            }
+        });
+        supports
+    }
+}
+
+pub trait CollaborationHub {
+    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator>;
+    fn user_participant_indices<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> &'a HashMap<u64, ParticipantIndex>;
+}
+
+impl CollaborationHub for ModelHandle<Project> {
+    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+        self.read(cx).collaborators()
+    }
+
+    fn user_participant_indices<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> &'a HashMap<u64, ParticipantIndex> {
+        self.read(cx).user_store().read(cx).participant_indices()
+    }
+}
+
+fn inlay_hint_settings(
+    location: Anchor,
+    snapshot: &MultiBufferSnapshot,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) -> InlayHintSettings {
+    let file = snapshot.file_at(location);
+    let language = snapshot.language_at(location);
+    let settings = all_language_settings(file, cx);
+    settings
+        .language(language.map(|l| l.name()).as_deref())
+        .inlay_hints
+}
+
+fn consume_contiguous_rows(
+    contiguous_row_selections: &mut Vec<Selection<Point>>,
+    selection: &Selection<Point>,
+    display_map: &DisplaySnapshot,
+    selections: &mut std::iter::Peekable<std::slice::Iter<Selection<Point>>>,
+) -> (u32, u32) {
+    contiguous_row_selections.push(selection.clone());
+    let start_row = selection.start.row;
+    let mut end_row = ending_row(selection, display_map);
+
+    while let Some(next_selection) = selections.peek() {
+        if next_selection.start.row <= end_row {
+            end_row = ending_row(next_selection, display_map);
+            contiguous_row_selections.push(selections.next().unwrap().clone());
+        } else {
+            break;
+        }
+    }
+    (start_row, end_row)
+}
+
+fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot) -> u32 {
+    if next_selection.end.column > 0 || next_selection.is_empty() {
+        display_map.next_line_boundary(next_selection.end).0.row + 1
+    } else {
+        next_selection.end.row
+    }
+}
+
+impl EditorSnapshot {
+    pub fn remote_selections_in_range<'a>(
+        &'a self,
+        range: &'a Range<Anchor>,
+        collaboration_hub: &dyn CollaborationHub,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = RemoteSelection> {
+        let participant_indices = collaboration_hub.user_participant_indices(cx);
+        let collaborators_by_peer_id = collaboration_hub.collaborators(cx);
+        let collaborators_by_replica_id = collaborators_by_peer_id
+            .iter()
+            .map(|(_, collaborator)| (collaborator.replica_id, collaborator))
+            .collect::<HashMap<_, _>>();
+        self.buffer_snapshot
+            .remote_selections_in_range(range)
+            .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| {
+                let collaborator = collaborators_by_replica_id.get(&replica_id)?;
+                let participant_index = participant_indices.get(&collaborator.user_id).copied();
+                Some(RemoteSelection {
+                    replica_id,
+                    selection,
+                    cursor_shape,
+                    line_mode,
+                    participant_index,
+                    peer_id: collaborator.peer_id,
+                })
+            })
+    }
+
+    pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
+        self.display_snapshot.buffer_snapshot.language_at(position)
+    }
+
+    pub fn is_focused(&self) -> bool {
+        self.is_focused
+    }
+
+    pub fn placeholder_text(&self) -> Option<&Arc<str>> {
+        self.placeholder_text.as_ref()
+    }
+
+    pub fn scroll_position(&self) -> Vector2F {
+        self.scroll_anchor.scroll_position(&self.display_snapshot)
+    }
+}
+
+impl Deref for EditorSnapshot {
+    type Target = DisplaySnapshot;
+
+    fn deref(&self) -> &Self::Target {
+        &self.display_snapshot
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Event {
+    InputIgnored {
+        text: Arc<str>,
+    },
+    InputHandled {
+        utf16_range_to_replace: Option<Range<isize>>,
+        text: Arc<str>,
+    },
+    ExcerptsAdded {
+        buffer: ModelHandle<Buffer>,
+        predecessor: ExcerptId,
+        excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
+    },
+    ExcerptsRemoved {
+        ids: Vec<ExcerptId>,
+    },
+    BufferEdited,
+    Edited,
+    Reparsed,
+    Focused,
+    Blurred,
+    DirtyChanged,
+    Saved,
+    TitleChanged,
+    DiffBaseChanged,
+    SelectionsChanged {
+        local: bool,
+    },
+    ScrollPositionChanged {
+        local: bool,
+        autoscroll: bool,
+    },
+    Closed,
+}
+
+pub struct EditorFocused(pub ViewHandle<Editor>);
+pub struct EditorBlurred(pub ViewHandle<Editor>);
+pub struct EditorReleased(pub WeakViewHandle<Editor>);
+
+impl Entity for Editor {
+    type Event = Event;
+
+    fn release(&mut self, cx: &mut AppContext) {
+        cx.emit_global(EditorReleased(self.handle.clone()));
+    }
+}
+
+impl View for Editor {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let style = self.style(cx);
+        let font_changed = self.display_map.update(cx, |map, cx| {
+            map.set_fold_ellipses_color(style.folds.ellipses.text_color);
+            map.set_font(style.text.font_id, style.text.font_size, cx)
+        });
+
+        if font_changed {
+            cx.defer(move |editor, cx: &mut ViewContext<Editor>| {
+                hide_hover(editor, cx);
+                hide_link_definition(editor, cx);
+            });
+        }
+
+        Stack::new()
+            .with_child(EditorElement::new(style.clone()))
+            .with_child(ChildView::new(&self.mouse_context_menu, cx))
+            .into_any()
+    }
+
+    fn ui_name() -> &'static str {
+        "Editor"
+    }
+
+    fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if cx.is_self_focused() {
+            let focused_event = EditorFocused(cx.handle());
+            cx.emit(Event::Focused);
+            cx.emit_global(focused_event);
+        }
+        if let Some(rename) = self.pending_rename.as_ref() {
+            cx.focus(&rename.editor);
+        } else if cx.is_self_focused() || !focused.is::<Editor>() {
+            if !self.focused {
+                self.blink_manager.update(cx, BlinkManager::enable);
+            }
+            self.focused = true;
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.finalize_last_transaction(cx);
+                if self.leader_peer_id.is_none() {
+                    buffer.set_active_selections(
+                        &self.selections.disjoint_anchors(),
+                        self.selections.line_mode,
+                        self.cursor_shape,
+                        cx,
+                    );
+                }
+            });
+        }
+    }
+
+    fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        let blurred_event = EditorBlurred(cx.handle());
+        cx.emit_global(blurred_event);
+        self.focused = false;
+        self.blink_manager.update(cx, BlinkManager::disable);
+        self.buffer
+            .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
+        self.hide_context_menu(cx);
+        hide_hover(self, cx);
+        cx.emit(Event::Blurred);
+        cx.notify();
+    }
+
+    fn modifiers_changed(
+        &mut self,
+        event: &gpui::platform::ModifiersChangedEvent,
+        cx: &mut ViewContext<Self>,
+    ) -> bool {
+        let pending_selection = self.has_pending_selection();
+
+        if let Some(point) = &self.link_go_to_definition_state.last_trigger_point {
+            if event.cmd && !pending_selection {
+                let point = point.clone();
+                let snapshot = self.snapshot(cx);
+                let kind = point.definition_kind(event.shift);
+
+                show_link_definition(kind, self, point, snapshot, cx);
+                return false;
+            }
+        }
+
+        {
+            if self.link_go_to_definition_state.symbol_range.is_some()
+                || !self.link_go_to_definition_state.definitions.is_empty()
+            {
+                self.link_go_to_definition_state.symbol_range.take();
+                self.link_go_to_definition_state.definitions.clear();
+                cx.notify();
+            }
+
+            self.link_go_to_definition_state.task = None;
+
+            self.clear_highlights::<LinkGoToDefinitionState>(cx);
+        }
+
+        false
+    }
+
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) {
+        Self::reset_to_default_keymap_context(keymap);
+        let mode = match self.mode {
+            EditorMode::SingleLine => "single_line",
+            EditorMode::AutoHeight { .. } => "auto_height",
+            EditorMode::Full => "full",
+        };
+        keymap.add_key("mode", mode);
+        if self.pending_rename.is_some() {
+            keymap.add_identifier("renaming");
+        }
+        if self.context_menu_visible() {
+            match self.context_menu.read().as_ref() {
+                Some(ContextMenu::Completions(_)) => {
+                    keymap.add_identifier("menu");
+                    keymap.add_identifier("showing_completions")
+                }
+                Some(ContextMenu::CodeActions(_)) => {
+                    keymap.add_identifier("menu");
+                    keymap.add_identifier("showing_code_actions")
+                }
+                None => {}
+            }
+        }
+
+        for layer in self.keymap_context_layers.values() {
+            keymap.extend(layer);
+        }
+
+        if let Some(extension) = self
+            .buffer
+            .read(cx)
+            .as_singleton()
+            .and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str())
+        {
+            keymap.add_key("extension", extension.to_string());
+        }
+    }
+
+    fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {
+        Some(
+            self.buffer
+                .read(cx)
+                .read(cx)
+                .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
+                .collect(),
+        )
+    }
+
+    fn selected_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
+        // Prevent the IME menu from appearing when holding down an alphabetic key
+        // while input is disabled.
+        if !self.input_enabled {
+            return None;
+        }
+
+        let range = self.selections.newest::<OffsetUtf16>(cx).range();
+        Some(range.start.0..range.end.0)
+    }
+
+    fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
+        let snapshot = self.buffer.read(cx).read(cx);
+        let range = self.text_highlights::<InputComposition>(cx)?.1.get(0)?;
+        Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0)
+    }
+
+    fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
+        self.clear_highlights::<InputComposition>(cx);
+        self.ime_transaction.take();
+    }
+
+    fn replace_text_in_range(
+        &mut self,
+        range_utf16: Option<Range<usize>>,
+        text: &str,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if !self.input_enabled {
+            cx.emit(Event::InputIgnored { text: text.into() });
+            return;
+        }
+
+        self.transact(cx, |this, cx| {
+            let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
+                let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
+                Some(this.selection_replacement_ranges(range_utf16, cx))
+            } else {
+                this.marked_text_ranges(cx)
+            };
+
+            let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| {
+                let newest_selection_id = this.selections.newest_anchor().id;
+                this.selections
+                    .all::<OffsetUtf16>(cx)
+                    .iter()
+                    .zip(ranges_to_replace.iter())
+                    .find_map(|(selection, range)| {
+                        if selection.id == newest_selection_id {
+                            Some(
+                                (range.start.0 as isize - selection.head().0 as isize)
+                                    ..(range.end.0 as isize - selection.head().0 as isize),
+                            )
+                        } else {
+                            None
+                        }
+                    })
+            });
+
+            cx.emit(Event::InputHandled {
+                utf16_range_to_replace: range_to_replace,
+                text: text.into(),
+            });
+
+            if let Some(new_selected_ranges) = new_selected_ranges {
+                this.change_selections(None, cx, |selections| {
+                    selections.select_ranges(new_selected_ranges)
+                });
+            }
+
+            this.handle_input(text, cx);
+        });
+
+        if let Some(transaction) = self.ime_transaction {
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.group_until_transaction(transaction, cx);
+            });
+        }
+
+        self.unmark_text(cx);
+    }
+
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range_utf16: Option<Range<usize>>,
+        text: &str,
+        new_selected_range_utf16: Option<Range<usize>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if !self.input_enabled {
+            cx.emit(Event::InputIgnored { text: text.into() });
+            return;
+        }
+
+        let transaction = self.transact(cx, |this, cx| {
+            let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) {
+                let snapshot = this.buffer.read(cx).read(cx);
+                if let Some(relative_range_utf16) = range_utf16.as_ref() {
+                    for marked_range in &mut marked_ranges {
+                        marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end;
+                        marked_range.start.0 += relative_range_utf16.start;
+                        marked_range.start =
+                            snapshot.clip_offset_utf16(marked_range.start, Bias::Left);
+                        marked_range.end =
+                            snapshot.clip_offset_utf16(marked_range.end, Bias::Right);
+                    }
+                }
+                Some(marked_ranges)
+            } else if let Some(range_utf16) = range_utf16 {
+                let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
+                Some(this.selection_replacement_ranges(range_utf16, cx))
+            } else {
+                None
+            };
+
+            let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| {
+                let newest_selection_id = this.selections.newest_anchor().id;
+                this.selections
+                    .all::<OffsetUtf16>(cx)
+                    .iter()
+                    .zip(ranges_to_replace.iter())
+                    .find_map(|(selection, range)| {
+                        if selection.id == newest_selection_id {
+                            Some(
+                                (range.start.0 as isize - selection.head().0 as isize)
+                                    ..(range.end.0 as isize - selection.head().0 as isize),
+                            )
+                        } else {
+                            None
+                        }
+                    })
+            });
+
+            cx.emit(Event::InputHandled {
+                utf16_range_to_replace: range_to_replace,
+                text: text.into(),
+            });
+
+            if let Some(ranges) = ranges_to_replace {
+                this.change_selections(None, cx, |s| s.select_ranges(ranges));
+            }
+
+            let marked_ranges = {
+                let snapshot = this.buffer.read(cx).read(cx);
+                this.selections
+                    .disjoint_anchors()
+                    .iter()
+                    .map(|selection| {
+                        selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot)
+                    })
+                    .collect::<Vec<_>>()
+            };
+
+            if text.is_empty() {
+                this.unmark_text(cx);
+            } else {
+                this.highlight_text::<InputComposition>(
+                    marked_ranges.clone(),
+                    this.style(cx).composition_mark,
+                    cx,
+                );
+            }
+
+            this.handle_input(text, cx);
+
+            if let Some(new_selected_range) = new_selected_range_utf16 {
+                let snapshot = this.buffer.read(cx).read(cx);
+                let new_selected_ranges = marked_ranges
+                    .into_iter()
+                    .map(|marked_range| {
+                        let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0;
+                        let new_start = OffsetUtf16(new_selected_range.start + insertion_start);
+                        let new_end = OffsetUtf16(new_selected_range.end + insertion_start);
+                        snapshot.clip_offset_utf16(new_start, Bias::Left)
+                            ..snapshot.clip_offset_utf16(new_end, Bias::Right)
+                    })
+                    .collect::<Vec<_>>();
+
+                drop(snapshot);
+                this.change_selections(None, cx, |selections| {
+                    selections.select_ranges(new_selected_ranges)
+                });
+            }
+        });
+
+        self.ime_transaction = self.ime_transaction.or(transaction);
+        if let Some(transaction) = self.ime_transaction {
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.group_until_transaction(transaction, cx);
+            });
+        }
+
+        if self.text_highlights::<InputComposition>(cx).is_none() {
+            self.ime_transaction.take();
+        }
+    }
+}
+
+fn build_style(
+    settings: &ThemeSettings,
+    get_field_editor_theme: Option<&GetFieldEditorTheme>,
+    override_text_style: Option<&OverrideTextStyle>,
+    cx: &AppContext,
+) -> EditorStyle {
+    let font_cache = cx.font_cache();
+    let line_height_scalar = settings.line_height();
+    let theme_id = settings.theme.meta.id;
+    let mut theme = settings.theme.editor.clone();
+    let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme {
+        let field_editor_theme = get_field_editor_theme(&settings.theme);
+        theme.text_color = field_editor_theme.text.color;
+        theme.selection = field_editor_theme.selection;
+        theme.background = field_editor_theme
+            .container
+            .background_color
+            .unwrap_or_default();
+        EditorStyle {
+            text: field_editor_theme.text,
+            placeholder_text: field_editor_theme.placeholder_text,
+            line_height_scalar,
+            theme,
+            theme_id,
+        }
+    } else {
+        let font_family_id = settings.buffer_font_family;
+        let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
+        let font_properties = Default::default();
+        let font_id = font_cache
+            .select_font(font_family_id, &font_properties)
+            .unwrap();
+        let font_size = settings.buffer_font_size(cx);
+        EditorStyle {
+            text: TextStyle {
+                color: settings.theme.editor.text_color,
+                font_family_name,
+                font_family_id,
+                font_id,
+                font_size,
+                font_properties,
+                underline: Default::default(),
+                soft_wrap: false,
+            },
+            placeholder_text: None,
+            line_height_scalar,
+            theme,
+            theme_id,
+        }
+    };
+
+    if let Some(highlight_style) = override_text_style.and_then(|build_style| build_style(&style)) {
+        if let Some(highlighted) = style
+            .text
+            .clone()
+            .highlight(highlight_style, font_cache)
+            .log_err()
+        {
+            style.text = highlighted;
+        }
+    }
+
+    style
+}
+
+trait SelectionExt {
+    fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize>;
+    fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point>;
+    fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint>;
+    fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot)
+        -> Range<u32>;
+}
+
+impl<T: ToPoint + ToOffset> SelectionExt for Selection<T> {
+    fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point> {
+        let start = self.start.to_point(buffer);
+        let end = self.end.to_point(buffer);
+        if self.reversed {
+            end..start
+        } else {
+            start..end
+        }
+    }
+
+    fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize> {
+        let start = self.start.to_offset(buffer);
+        let end = self.end.to_offset(buffer);
+        if self.reversed {
+            end..start
+        } else {
+            start..end
+        }
+    }
+
+    fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint> {
+        let start = self
+            .start
+            .to_point(&map.buffer_snapshot)
+            .to_display_point(map);
+        let end = self
+            .end
+            .to_point(&map.buffer_snapshot)
+            .to_display_point(map);
+        if self.reversed {
+            end..start
+        } else {
+            start..end
+        }
+    }
+
+    fn spanned_rows(
+        &self,
+        include_end_if_at_line_start: bool,
+        map: &DisplaySnapshot,
+    ) -> Range<u32> {
+        let start = self.start.to_point(&map.buffer_snapshot);
+        let mut end = self.end.to_point(&map.buffer_snapshot);
+        if !include_end_if_at_line_start && start.row != end.row && end.column == 0 {
+            end.row -= 1;
+        }
+
+        let buffer_start = map.prev_line_boundary(start).0;
+        let buffer_end = map.next_line_boundary(end).0;
+        buffer_start.row..buffer_end.row + 1
+    }
+}
+
+impl<T: InvalidationRegion> InvalidationStack<T> {
+    fn invalidate<S>(&mut self, selections: &[Selection<S>], buffer: &MultiBufferSnapshot)
+    where
+        S: Clone + ToOffset,
+    {
+        while let Some(region) = self.last() {
+            let all_selections_inside_invalidation_ranges =
+                if selections.len() == region.ranges().len() {
+                    selections
+                        .iter()
+                        .zip(region.ranges().iter().map(|r| r.to_offset(buffer)))
+                        .all(|(selection, invalidation_range)| {
+                            let head = selection.head().to_offset(buffer);
+                            invalidation_range.start <= head && invalidation_range.end >= head
+                        })
+                } else {
+                    false
+                };
+
+            if all_selections_inside_invalidation_ranges {
+                break;
+            } else {
+                self.pop();
+            }
+        }
+    }
+}
+
+impl<T> Default for InvalidationStack<T> {
+    fn default() -> Self {
+        Self(Default::default())
+    }
+}
+
+impl<T> Deref for InvalidationStack<T> {
+    type Target = Vec<T>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl<T> DerefMut for InvalidationStack<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl InvalidationRegion for SnippetState {
+    fn ranges(&self) -> &[Range<Anchor>] {
+        &self.ranges[self.active_index]
+    }
+}
+
+impl Deref for EditorStyle {
+    type Target = theme::Editor;
+
+    fn deref(&self) -> &Self::Target {
+        &self.theme
+    }
+}
+
+pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock {
+    let mut highlighted_lines = Vec::new();
+
+    for (index, line) in diagnostic.message.lines().enumerate() {
+        let line = match &diagnostic.source {
+            Some(source) if index == 0 => {
+                let source_highlight = Vec::from_iter(0..source.len());
+                highlight_diagnostic_message(source_highlight, &format!("{source}: {line}"))
+            }
+
+            _ => highlight_diagnostic_message(Vec::new(), line),
+        };
+        highlighted_lines.push(line);
+    }
+    let message = diagnostic.message;
+    Arc::new(move |cx: &mut BlockContext| {
+        let message = message.clone();
+        let settings = settings::get::<ThemeSettings>(cx);
+        let tooltip_style = settings.theme.tooltip.clone();
+        let theme = &settings.theme.editor;
+        let style = diagnostic_style(diagnostic.severity, is_valid, theme);
+        let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
+        let anchor_x = cx.anchor_x;
+        enum BlockContextToolip {}
+        MouseEventHandler::new::<BlockContext, _>(cx.block_id, cx, |_, _| {
+            Flex::column()
+                .with_children(highlighted_lines.iter().map(|(line, highlights)| {
+                    Label::new(
+                        line.clone(),
+                        style.message.clone().with_font_size(font_size),
+                    )
+                    .with_highlights(highlights.clone())
+                    .contained()
+                    .with_margin_left(anchor_x)
+                }))
+                .aligned()
+                .left()
+                .into_any()
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, _, cx| {
+            cx.write_to_clipboard(ClipboardItem::new(message.clone()));
+        })
+        // We really need to rethink this ID system...
+        .with_tooltip::<BlockContextToolip>(
+            cx.block_id,
+            "Copy diagnostic message",
+            None,
+            tooltip_style,
+            cx,
+        )
+        .into_any()
+    })
+}
+
+pub fn highlight_diagnostic_message(
+    initial_highlights: Vec<usize>,
+    message: &str,
+) -> (String, Vec<usize>) {
+    let mut message_without_backticks = String::new();
+    let mut prev_offset = 0;
+    let mut inside_block = false;
+    let mut highlights = initial_highlights;
+    for (match_ix, (offset, _)) in message
+        .match_indices('`')
+        .chain([(message.len(), "")])
+        .enumerate()
+    {
+        message_without_backticks.push_str(&message[prev_offset..offset]);
+        if inside_block {
+            highlights.extend(prev_offset - match_ix..offset - match_ix);
+        }
+
+        inside_block = !inside_block;
+        prev_offset = offset + 1;
+    }
+
+    (message_without_backticks, highlights)
+}
+
+pub fn diagnostic_style(
+    severity: DiagnosticSeverity,
+    valid: bool,
+    theme: &theme::Editor,
+) -> DiagnosticStyle {
+    match (severity, valid) {
+        (DiagnosticSeverity::ERROR, true) => theme.error_diagnostic.clone(),
+        (DiagnosticSeverity::ERROR, false) => theme.invalid_error_diagnostic.clone(),
+        (DiagnosticSeverity::WARNING, true) => theme.warning_diagnostic.clone(),
+        (DiagnosticSeverity::WARNING, false) => theme.invalid_warning_diagnostic.clone(),
+        (DiagnosticSeverity::INFORMATION, true) => theme.information_diagnostic.clone(),
+        (DiagnosticSeverity::INFORMATION, false) => theme.invalid_information_diagnostic.clone(),
+        (DiagnosticSeverity::HINT, true) => theme.hint_diagnostic.clone(),
+        (DiagnosticSeverity::HINT, false) => theme.invalid_hint_diagnostic.clone(),
+        _ => theme.invalid_hint_diagnostic.clone(),
+    }
+}
+
+pub fn combine_syntax_and_fuzzy_match_highlights(
+    text: &str,
+    default_style: HighlightStyle,
+    syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
+    match_indices: &[usize],
+) -> Vec<(Range<usize>, HighlightStyle)> {
+    let mut result = Vec::new();
+    let mut match_indices = match_indices.iter().copied().peekable();
+
+    for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())])
+    {
+        syntax_highlight.weight = None;
+
+        // Add highlights for any fuzzy match characters before the next
+        // syntax highlight range.
+        while let Some(&match_index) = match_indices.peek() {
+            if match_index >= range.start {
+                break;
+            }
+            match_indices.next();
+            let end_index = char_ix_after(match_index, text);
+            let mut match_style = default_style;
+            match_style.weight = Some(fonts::Weight::BOLD);
+            result.push((match_index..end_index, match_style));
+        }
+
+        if range.start == usize::MAX {
+            break;
+        }
+
+        // Add highlights for any fuzzy match characters within the
+        // syntax highlight range.
+        let mut offset = range.start;
+        while let Some(&match_index) = match_indices.peek() {
+            if match_index >= range.end {
+                break;
+            }
+
+            match_indices.next();
+            if match_index > offset {
+                result.push((offset..match_index, syntax_highlight));
+            }
+
+            let mut end_index = char_ix_after(match_index, text);
+            while let Some(&next_match_index) = match_indices.peek() {
+                if next_match_index == end_index && next_match_index < range.end {
+                    end_index = char_ix_after(next_match_index, text);
+                    match_indices.next();
+                } else {
+                    break;
+                }
+            }
+
+            let mut match_style = syntax_highlight;
+            match_style.weight = Some(fonts::Weight::BOLD);
+            result.push((match_index..end_index, match_style));
+            offset = end_index;
+        }
+
+        if offset < range.end {
+            result.push((offset..range.end, syntax_highlight));
+        }
+    }
+
+    fn char_ix_after(ix: usize, text: &str) -> usize {
+        ix + text[ix..].chars().next().unwrap().len_utf8()
+    }
+
+    result
+}
+
+pub fn styled_runs_for_code_label<'a>(
+    label: &'a CodeLabel,
+    syntax_theme: &'a theme::SyntaxTheme,
+) -> impl 'a + Iterator<Item = (Range<usize>, HighlightStyle)> {
+    let fade_out = HighlightStyle {
+        fade_out: Some(0.35),
+        ..Default::default()
+    };
+
+    let mut prev_end = label.filter_range.end;
+    label
+        .runs
+        .iter()
+        .enumerate()
+        .flat_map(move |(ix, (range, highlight_id))| {
+            let style = if let Some(style) = highlight_id.style(syntax_theme) {
+                style
+            } else {
+                return Default::default();
+            };
+            let mut muted_style = style;
+            muted_style.highlight(fade_out);
+
+            let mut runs = SmallVec::<[(Range<usize>, HighlightStyle); 3]>::new();
+            if range.start >= label.filter_range.end {
+                if range.start > prev_end {
+                    runs.push((prev_end..range.start, fade_out));
+                }
+                runs.push((range.clone(), muted_style));
+            } else if range.end <= label.filter_range.end {
+                runs.push((range.clone(), style));
+            } else {
+                runs.push((range.start..label.filter_range.end, style));
+                runs.push((label.filter_range.end..range.end, muted_style));
+            }
+            prev_end = cmp::max(prev_end, range.end);
+
+            if ix + 1 == label.runs.len() && label.text.len() > prev_end {
+                runs.push((prev_end..label.text.len(), fade_out));
+            }
+
+            runs
+        })
+}
+
+pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a {
+    let mut index = 0;
+    let mut codepoints = text.char_indices().peekable();
+
+    std::iter::from_fn(move || {
+        let start_index = index;
+        while let Some((new_index, codepoint)) = codepoints.next() {
+            index = new_index + codepoint.len_utf8();
+            let current_upper = codepoint.is_uppercase();
+            let next_upper = codepoints
+                .peek()
+                .map(|(_, c)| c.is_uppercase())
+                .unwrap_or(false);
+
+            if !current_upper && next_upper {
+                return Some(&text[start_index..index]);
+            }
+        }
+
+        index = text.len();
+        if start_index < text.len() {
+            return Some(&text[start_index..]);
+        }
+        None
+    })
+    .flat_map(|word| word.split_inclusive('_'))
+    .flat_map(|word| word.split_inclusive('-'))
+}
+
+trait RangeToAnchorExt {
+    fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
+}
+
+impl<T: ToOffset> RangeToAnchorExt for Range<T> {
+    fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor> {
+        snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end)
+    }
+}

crates/editor2/src/editor_settings.rs ๐Ÿ”—

@@ -0,0 +1,62 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Deserialize)]
+pub struct EditorSettings {
+    pub cursor_blink: bool,
+    pub hover_popover_enabled: bool,
+    pub show_completions_on_input: bool,
+    pub show_completion_documentation: bool,
+    pub use_on_type_format: bool,
+    pub scrollbar: Scrollbar,
+    pub relative_line_numbers: bool,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct Scrollbar {
+    pub show: ShowScrollbar,
+    pub git_diff: bool,
+    pub selections: bool,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowScrollbar {
+    Auto,
+    System,
+    Always,
+    Never,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct EditorSettingsContent {
+    pub cursor_blink: Option<bool>,
+    pub hover_popover_enabled: Option<bool>,
+    pub show_completions_on_input: Option<bool>,
+    pub show_completion_documentation: Option<bool>,
+    pub use_on_type_format: Option<bool>,
+    pub scrollbar: Option<ScrollbarContent>,
+    pub relative_line_numbers: Option<bool>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ScrollbarContent {
+    pub show: Option<ShowScrollbar>,
+    pub git_diff: Option<bool>,
+    pub selections: Option<bool>,
+}
+
+impl Setting for EditorSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = EditorSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/editor2/src/editor_tests.rs ๐Ÿ”—

@@ -0,0 +1,8195 @@
+use super::*;
+use crate::{
+    scroll::scroll_amount::ScrollAmount,
+    test::{
+        assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
+        editor_test_context::EditorTestContext, select_ranges,
+    },
+    JoinLines,
+};
+use drag_and_drop::DragAndDrop;
+use futures::StreamExt;
+use gpui::{
+    executor::Deterministic,
+    geometry::{rect::RectF, vector::vec2f},
+    platform::{WindowBounds, WindowOptions},
+    serde_json::{self, json},
+    TestAppContext,
+};
+use indoc::indoc;
+use language::{
+    language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
+    BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
+    Override, Point,
+};
+use parking_lot::Mutex;
+use project::project_settings::{LspSettings, ProjectSettings};
+use project::FakeFs;
+use std::sync::atomic;
+use std::sync::atomic::AtomicUsize;
+use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
+use unindent::Unindent;
+use util::{
+    assert_set_eq,
+    test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
+};
+use workspace::{
+    item::{FollowableItem, Item, ItemHandle},
+    NavigationEntry, ViewId,
+};
+
+#[gpui::test]
+fn test_edit_events(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let buffer = cx.add_model(|cx| {
+        let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456");
+        buffer.set_group_interval(Duration::from_secs(1));
+        buffer
+    });
+
+    let events = Rc::new(RefCell::new(Vec::new()));
+    let editor1 = cx
+        .add_window({
+            let events = events.clone();
+            |cx| {
+                cx.subscribe(&cx.handle(), move |_, _, event, _| {
+                    if matches!(
+                        event,
+                        Event::Edited | Event::BufferEdited | Event::DirtyChanged
+                    ) {
+                        events.borrow_mut().push(("editor1", event.clone()));
+                    }
+                })
+                .detach();
+                Editor::for_buffer(buffer.clone(), None, cx)
+            }
+        })
+        .root(cx);
+    let editor2 = cx
+        .add_window({
+            let events = events.clone();
+            |cx| {
+                cx.subscribe(&cx.handle(), move |_, _, event, _| {
+                    if matches!(
+                        event,
+                        Event::Edited | Event::BufferEdited | Event::DirtyChanged
+                    ) {
+                        events.borrow_mut().push(("editor2", event.clone()));
+                    }
+                })
+                .detach();
+                Editor::for_buffer(buffer.clone(), None, cx)
+            }
+        })
+        .root(cx);
+    assert_eq!(mem::take(&mut *events.borrow_mut()), []);
+
+    // Mutating editor 1 will emit an `Edited` event only for that editor.
+    editor1.update(cx, |editor, cx| editor.insert("X", cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor1", Event::Edited),
+            ("editor1", Event::BufferEdited),
+            ("editor2", Event::BufferEdited),
+            ("editor1", Event::DirtyChanged),
+            ("editor2", Event::DirtyChanged)
+        ]
+    );
+
+    // Mutating editor 2 will emit an `Edited` event only for that editor.
+    editor2.update(cx, |editor, cx| editor.delete(&Delete, cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor2", Event::Edited),
+            ("editor1", Event::BufferEdited),
+            ("editor2", Event::BufferEdited),
+        ]
+    );
+
+    // Undoing on editor 1 will emit an `Edited` event only for that editor.
+    editor1.update(cx, |editor, cx| editor.undo(&Undo, cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor1", Event::Edited),
+            ("editor1", Event::BufferEdited),
+            ("editor2", Event::BufferEdited),
+            ("editor1", Event::DirtyChanged),
+            ("editor2", Event::DirtyChanged),
+        ]
+    );
+
+    // Redoing on editor 1 will emit an `Edited` event only for that editor.
+    editor1.update(cx, |editor, cx| editor.redo(&Redo, cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor1", Event::Edited),
+            ("editor1", Event::BufferEdited),
+            ("editor2", Event::BufferEdited),
+            ("editor1", Event::DirtyChanged),
+            ("editor2", Event::DirtyChanged),
+        ]
+    );
+
+    // Undoing on editor 2 will emit an `Edited` event only for that editor.
+    editor2.update(cx, |editor, cx| editor.undo(&Undo, cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor2", Event::Edited),
+            ("editor1", Event::BufferEdited),
+            ("editor2", Event::BufferEdited),
+            ("editor1", Event::DirtyChanged),
+            ("editor2", Event::DirtyChanged),
+        ]
+    );
+
+    // Redoing on editor 2 will emit an `Edited` event only for that editor.
+    editor2.update(cx, |editor, cx| editor.redo(&Redo, cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor2", Event::Edited),
+            ("editor1", Event::BufferEdited),
+            ("editor2", Event::BufferEdited),
+            ("editor1", Event::DirtyChanged),
+            ("editor2", Event::DirtyChanged),
+        ]
+    );
+
+    // No event is emitted when the mutation is a no-op.
+    editor2.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
+
+        editor.backspace(&Backspace, cx);
+    });
+    assert_eq!(mem::take(&mut *events.borrow_mut()), []);
+}
+
+#[gpui::test]
+fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut now = Instant::now();
+    let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456"));
+    let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let editor = cx
+        .add_window(|cx| build_editor(buffer.clone(), cx))
+        .root(cx);
+
+    editor.update(cx, |editor, cx| {
+        editor.start_transaction_at(now, cx);
+        editor.change_selections(None, cx, |s| s.select_ranges([2..4]));
+
+        editor.insert("cd", cx);
+        editor.end_transaction_at(now, cx);
+        assert_eq!(editor.text(cx), "12cd56");
+        assert_eq!(editor.selections.ranges(cx), vec![4..4]);
+
+        editor.start_transaction_at(now, cx);
+        editor.change_selections(None, cx, |s| s.select_ranges([4..5]));
+        editor.insert("e", cx);
+        editor.end_transaction_at(now, cx);
+        assert_eq!(editor.text(cx), "12cde6");
+        assert_eq!(editor.selections.ranges(cx), vec![5..5]);
+
+        now += group_interval + Duration::from_millis(1);
+        editor.change_selections(None, cx, |s| s.select_ranges([2..2]));
+
+        // Simulate an edit in another editor
+        buffer.update(cx, |buffer, cx| {
+            buffer.start_transaction_at(now, cx);
+            buffer.edit([(0..1, "a")], None, cx);
+            buffer.edit([(1..1, "b")], None, cx);
+            buffer.end_transaction_at(now, cx);
+        });
+
+        assert_eq!(editor.text(cx), "ab2cde6");
+        assert_eq!(editor.selections.ranges(cx), vec![3..3]);
+
+        // Last transaction happened past the group interval in a different editor.
+        // Undo it individually and don't restore selections.
+        editor.undo(&Undo, cx);
+        assert_eq!(editor.text(cx), "12cde6");
+        assert_eq!(editor.selections.ranges(cx), vec![2..2]);
+
+        // First two transactions happened within the group interval in this editor.
+        // Undo them together and restore selections.
+        editor.undo(&Undo, cx);
+        editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op.
+        assert_eq!(editor.text(cx), "123456");
+        assert_eq!(editor.selections.ranges(cx), vec![0..0]);
+
+        // Redo the first two transactions together.
+        editor.redo(&Redo, cx);
+        assert_eq!(editor.text(cx), "12cde6");
+        assert_eq!(editor.selections.ranges(cx), vec![5..5]);
+
+        // Redo the last transaction on its own.
+        editor.redo(&Redo, cx);
+        assert_eq!(editor.text(cx), "ab2cde6");
+        assert_eq!(editor.selections.ranges(cx), vec![6..6]);
+
+        // Test empty transactions.
+        editor.start_transaction_at(now, cx);
+        editor.end_transaction_at(now, cx);
+        editor.undo(&Undo, cx);
+        assert_eq!(editor.text(cx), "12cde6");
+    });
+}
+
+#[gpui::test]
+fn test_ime_composition(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let buffer = cx.add_model(|cx| {
+        let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde");
+        // Ensure automatic grouping doesn't occur.
+        buffer.set_group_interval(Duration::ZERO);
+        buffer
+    });
+
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    cx.add_window(|cx| {
+        let mut editor = build_editor(buffer.clone(), cx);
+
+        // Start a new IME composition.
+        editor.replace_and_mark_text_in_range(Some(0..1), "ร ", None, cx);
+        editor.replace_and_mark_text_in_range(Some(0..1), "รก", None, cx);
+        editor.replace_and_mark_text_in_range(Some(0..1), "รค", None, cx);
+        assert_eq!(editor.text(cx), "รคbcde");
+        assert_eq!(
+            editor.marked_text_ranges(cx),
+            Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
+        );
+
+        // Finalize IME composition.
+        editor.replace_text_in_range(None, "ฤ", cx);
+        assert_eq!(editor.text(cx), "ฤbcde");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+
+        // IME composition edits are grouped and are undone/redone at once.
+        editor.undo(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "abcde");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+        editor.redo(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "ฤbcde");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+
+        // Start a new IME composition.
+        editor.replace_and_mark_text_in_range(Some(0..1), "ร ", None, cx);
+        assert_eq!(
+            editor.marked_text_ranges(cx),
+            Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
+        );
+
+        // Undoing during an IME composition cancels it.
+        editor.undo(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "ฤbcde");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+
+        // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
+        editor.replace_and_mark_text_in_range(Some(4..999), "รจ", None, cx);
+        assert_eq!(editor.text(cx), "ฤbcdรจ");
+        assert_eq!(
+            editor.marked_text_ranges(cx),
+            Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
+        );
+
+        // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
+        editor.replace_text_in_range(Some(4..999), "ฤ™", cx);
+        assert_eq!(editor.text(cx), "ฤbcdฤ™");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+
+        // Start a new IME composition with multiple cursors.
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([
+                OffsetUtf16(1)..OffsetUtf16(1),
+                OffsetUtf16(3)..OffsetUtf16(3),
+                OffsetUtf16(5)..OffsetUtf16(5),
+            ])
+        });
+        editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx);
+        assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
+        assert_eq!(
+            editor.marked_text_ranges(cx),
+            Some(vec![
+                OffsetUtf16(0)..OffsetUtf16(3),
+                OffsetUtf16(4)..OffsetUtf16(7),
+                OffsetUtf16(8)..OffsetUtf16(11)
+            ])
+        );
+
+        // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
+        editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx);
+        assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
+        assert_eq!(
+            editor.marked_text_ranges(cx),
+            Some(vec![
+                OffsetUtf16(1)..OffsetUtf16(2),
+                OffsetUtf16(5)..OffsetUtf16(6),
+                OffsetUtf16(9)..OffsetUtf16(10)
+            ])
+        );
+
+        // Finalize IME composition with multiple cursors.
+        editor.replace_text_in_range(Some(9..10), "2", cx);
+        assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+
+        editor
+    });
+}
+
+#[gpui::test]
+fn test_selection_with_mouse(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    editor.update(cx, |view, cx| {
+        view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
+    });
+    assert_eq!(
+        editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
+    );
+
+    editor.update(cx, |view, cx| {
+        view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx);
+    });
+
+    assert_eq!(
+        editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
+    );
+
+    editor.update(cx, |view, cx| {
+        view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx);
+    });
+
+    assert_eq!(
+        editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
+    );
+
+    editor.update(cx, |view, cx| {
+        view.end_selection(cx);
+        view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx);
+    });
+
+    assert_eq!(
+        editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
+    );
+
+    editor.update(cx, |view, cx| {
+        view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
+        view.update_selection(DisplayPoint::new(0, 0), 0, Vector2F::zero(), cx);
+    });
+
+    assert_eq!(
+        editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        [
+            DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1),
+            DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)
+        ]
+    );
+
+    editor.update(cx, |view, cx| {
+        view.end_selection(cx);
+    });
+
+    assert_eq!(
+        editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
+    );
+}
+
+#[gpui::test]
+fn test_canceling_pending_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+
+    view.update(cx, |view, cx| {
+        view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.cancel(&Cancel, cx);
+        view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_clone(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let (text, selection_ranges) = marked_text_ranges(
+        indoc! {"
+            one
+            two
+            threeห‡
+            four
+            fiveห‡
+        "},
+        true,
+    );
+
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&text, cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+
+    editor.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
+        editor.fold_ranges(
+            [
+                Point::new(1, 0)..Point::new(2, 0),
+                Point::new(3, 0)..Point::new(4, 0),
+            ],
+            true,
+            cx,
+        );
+    });
+
+    let cloned_editor = editor
+        .update(cx, |editor, cx| {
+            cx.add_window(Default::default(), |cx| editor.clone(cx))
+        })
+        .root(cx);
+
+    let snapshot = editor.update(cx, |e, cx| e.snapshot(cx));
+    let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx));
+
+    assert_eq!(
+        cloned_editor.update(cx, |e, cx| e.display_text(cx)),
+        editor.update(cx, |e, cx| e.display_text(cx))
+    );
+    assert_eq!(
+        cloned_snapshot
+            .folds_in_range(0..text.len())
+            .collect::<Vec<_>>(),
+        snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
+    );
+    assert_set_eq!(
+        cloned_editor.read_with(cx, |editor, cx| editor.selections.ranges::<Point>(cx)),
+        editor.read_with(cx, |editor, cx| editor.selections.ranges(cx))
+    );
+    assert_set_eq!(
+        cloned_editor.update(cx, |e, cx| e.selections.display_ranges(cx)),
+        editor.update(cx, |e, cx| e.selections.display_ranges(cx))
+    );
+}
+
+#[gpui::test]
+async fn test_navigation_history(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    cx.set_global(DragAndDrop::<Workspace>::default());
+    use workspace::item::Item;
+
+    let fs = FakeFs::new(cx.background());
+    let project = Project::test(fs, [], cx).await;
+    let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+    let workspace = window.root(cx);
+    let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+    window.add_view(cx, |cx| {
+        let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
+        let mut editor = build_editor(buffer.clone(), cx);
+        let handle = cx.handle();
+        editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
+
+        fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
+            editor.nav_history.as_mut().unwrap().pop_backward(cx)
+        }
+
+        // Move the cursor a small distance.
+        // Nothing is added to the navigation history.
+        editor.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+        });
+        editor.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
+        });
+        assert!(pop_history(&mut editor, cx).is_none());
+
+        // Move the cursor a large distance.
+        // The history can jump back to the previous position.
+        editor.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
+        });
+        let nav_entry = pop_history(&mut editor, cx).unwrap();
+        editor.navigate(nav_entry.data.unwrap(), cx);
+        assert_eq!(nav_entry.item.id(), cx.view_id());
+        assert_eq!(
+            editor.selections.display_ranges(cx),
+            &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
+        );
+        assert!(pop_history(&mut editor, cx).is_none());
+
+        // Move the cursor a small distance via the mouse.
+        // Nothing is added to the navigation history.
+        editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
+        editor.end_selection(cx);
+        assert_eq!(
+            editor.selections.display_ranges(cx),
+            &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+        );
+        assert!(pop_history(&mut editor, cx).is_none());
+
+        // Move the cursor a large distance via the mouse.
+        // The history can jump back to the previous position.
+        editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
+        editor.end_selection(cx);
+        assert_eq!(
+            editor.selections.display_ranges(cx),
+            &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
+        );
+        let nav_entry = pop_history(&mut editor, cx).unwrap();
+        editor.navigate(nav_entry.data.unwrap(), cx);
+        assert_eq!(nav_entry.item.id(), cx.view_id());
+        assert_eq!(
+            editor.selections.display_ranges(cx),
+            &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+        );
+        assert!(pop_history(&mut editor, cx).is_none());
+
+        // Set scroll position to check later
+        editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
+        let original_scroll_position = editor.scroll_manager.anchor();
+
+        // Jump to the end of the document and adjust scroll
+        editor.move_to_end(&MoveToEnd, cx);
+        editor.set_scroll_position(Vector2F::new(-2.5, -0.5), cx);
+        assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
+
+        let nav_entry = pop_history(&mut editor, cx).unwrap();
+        editor.navigate(nav_entry.data.unwrap(), cx);
+        assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
+
+        // Ensure we don't panic when navigation data contains invalid anchors *and* points.
+        let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
+        invalid_anchor.text_anchor.buffer_id = Some(999);
+        let invalid_point = Point::new(9999, 0);
+        editor.navigate(
+            Box::new(NavigationData {
+                cursor_anchor: invalid_anchor,
+                cursor_position: invalid_point,
+                scroll_anchor: ScrollAnchor {
+                    anchor: invalid_anchor,
+                    offset: Default::default(),
+                },
+                scroll_top_row: invalid_point.row,
+            }),
+            cx,
+        );
+        assert_eq!(
+            editor.selections.display_ranges(cx),
+            &[editor.max_point(cx)..editor.max_point(cx)]
+        );
+        assert_eq!(
+            editor.scroll_position(cx),
+            vec2f(0., editor.max_point(cx).row() as f32)
+        );
+
+        editor
+    });
+}
+
+#[gpui::test]
+fn test_cancel(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+
+    view.update(cx, |view, cx| {
+        view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
+        view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx);
+        view.end_selection(cx);
+
+        view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
+        view.update_selection(DisplayPoint::new(0, 3), 0, Vector2F::zero(), cx);
+        view.end_selection(cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.cancel(&Cancel, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.cancel(&Cancel, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_fold_action(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(
+                &"
+                impl Foo {
+                    // Hello!
+
+                    fn a() {
+                        1
+                    }
+
+                    fn b() {
+                        2
+                    }
+
+                    fn c() {
+                        3
+                    }
+                }
+            "
+                .unindent(),
+                cx,
+            );
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]);
+        });
+        view.fold(&Fold, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                impl Foo {
+                    // Hello!
+
+                    fn a() {
+                        1
+                    }
+
+                    fn b() {โ‹ฏ
+                    }
+
+                    fn c() {โ‹ฏ
+                    }
+                }
+            "
+            .unindent(),
+        );
+
+        view.fold(&Fold, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                impl Foo {โ‹ฏ
+                }
+            "
+            .unindent(),
+        );
+
+        view.unfold_lines(&UnfoldLines, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                impl Foo {
+                    // Hello!
+
+                    fn a() {
+                        1
+                    }
+
+                    fn b() {โ‹ฏ
+                    }
+
+                    fn c() {โ‹ฏ
+                    }
+                }
+            "
+            .unindent(),
+        );
+
+        view.unfold_lines(&UnfoldLines, cx);
+        assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
+    });
+}
+
+#[gpui::test]
+fn test_move_cursor(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
+    let view = cx
+        .add_window(|cx| build_editor(buffer.clone(), cx))
+        .root(cx);
+
+    buffer.update(cx, |buffer, cx| {
+        buffer.edit(
+            vec![
+                (Point::new(1, 0)..Point::new(1, 0), "\t"),
+                (Point::new(1, 1)..Point::new(1, 1), "\t"),
+            ],
+            None,
+            cx,
+        );
+    });
+    view.update(cx, |view, cx| {
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+        );
+
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
+        );
+
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
+        );
+
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
+        );
+
+        view.move_up(&MoveUp, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+        );
+
+        view.move_to_end(&MoveToEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)]
+        );
+
+        view.move_to_beginning(&MoveToBeginning, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+        );
+
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]);
+        });
+        view.select_to_beginning(&SelectToBeginning, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)]
+        );
+
+        view.select_to_end(&SelectToEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("โ“โ“‘โ“’โ““โ“”\nabcde\nฮฑฮฒฮณฮดฮต", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
+
+    assert_eq!('โ“'.len_utf8(), 3);
+    assert_eq!('ฮฑ'.len_utf8(), 2);
+
+    view.update(cx, |view, cx| {
+        view.fold_ranges(
+            vec![
+                Point::new(0, 6)..Point::new(0, 12),
+                Point::new(1, 2)..Point::new(1, 4),
+                Point::new(2, 4)..Point::new(2, 8),
+            ],
+            true,
+            cx,
+        );
+        assert_eq!(view.display_text(cx), "โ“โ“‘โ‹ฏโ“”\nabโ‹ฏe\nฮฑฮฒโ‹ฏฮต");
+
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "โ“".len())]
+        );
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "โ“โ“‘".len())]
+        );
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "โ“โ“‘โ‹ฏ".len())]
+        );
+
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "abโ‹ฏe".len())]
+        );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "abโ‹ฏ".len())]
+        );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "ab".len())]
+        );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "a".len())]
+        );
+
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "ฮฑ".len())]
+        );
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "ฮฑฮฒ".len())]
+        );
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "ฮฑฮฒโ‹ฏ".len())]
+        );
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "ฮฑฮฒโ‹ฏฮต".len())]
+        );
+
+        view.move_up(&MoveUp, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "abโ‹ฏe".len())]
+        );
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "ฮฑฮฒโ‹ฏฮต".len())]
+        );
+        view.move_up(&MoveUp, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "abโ‹ฏe".len())]
+        );
+
+        view.move_up(&MoveUp, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "โ“โ“‘".len())]
+        );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "โ“".len())]
+        );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "".len())]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("โ“โ“‘โ“’โ““โ“”\nabcd\nฮฑฮฒฮณ\nabcd\nโ“โ“‘โ“’โ““โ“”\n", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([empty_range(0, "โ“โ“‘โ“’โ““โ“”".len())]);
+        });
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "abcd".len())]
+        );
+
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "ฮฑฮฒฮณ".len())]
+        );
+
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(3, "abcd".len())]
+        );
+
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(4, "โ“โ“‘โ“’โ““โ“”".len())]
+        );
+
+        view.move_up(&MoveUp, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(3, "abcd".len())]
+        );
+
+        view.move_up(&MoveUp, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "ฮฑฮฒฮณ".len())]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_beginning_end_of_line(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\n  def", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
+            ]);
+        });
+    });
+
+    view.update(cx, |view, cx| {
+        view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.move_to_end_of_line(&MoveToEndOfLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
+            ]
+        );
+    });
+
+    // Moving to the end of line again is a no-op.
+    view.update(cx, |view, cx| {
+        view.move_to_end_of_line(&MoveToEndOfLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.move_left(&MoveLeft, cx);
+        view.select_to_beginning_of_line(
+            &SelectToBeginningOfLine {
+                stop_at_soft_wraps: true,
+            },
+            cx,
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.select_to_beginning_of_line(
+            &SelectToBeginningOfLine {
+                stop_at_soft_wraps: true,
+            },
+            cx,
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.select_to_beginning_of_line(
+            &SelectToBeginningOfLine {
+                stop_at_soft_wraps: true,
+            },
+            cx,
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.select_to_end_of_line(
+            &SelectToEndOfLine {
+                stop_at_soft_wraps: true,
+            },
+            cx,
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.delete_to_end_of_line(&DeleteToEndOfLine, cx);
+        assert_eq!(view.display_text(cx), "ab\n  de");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
+        assert_eq!(view.display_text(cx), "\n");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+            ]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n  {baz.qux()}", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
+                DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
+            ])
+        });
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_selection_ranges("use std::ห‡str::{foo, bar}\n\n  {ห‡baz.qux()}", view, cx);
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_selection_ranges("use stdห‡::str::{foo, bar}\n\n  ห‡{baz.qux()}", view, cx);
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_selection_ranges("use ห‡std::str::{foo, bar}\n\nห‡  {baz.qux()}", view, cx);
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_selection_ranges("ห‡use std::str::{foo, bar}\nห‡\n  {baz.qux()}", view, cx);
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_selection_ranges("ห‡use std::str::{foo, barห‡}\n\n  {baz.qux()}", view, cx);
+
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_selection_ranges("useห‡ std::str::{foo, bar}ห‡\n\n  {baz.qux()}", view, cx);
+
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_selection_ranges("use stdห‡::str::{foo, bar}\nห‡\n  {baz.qux()}", view, cx);
+
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_selection_ranges("use std::ห‡str::{foo, bar}\n\n  {ห‡baz.qux()}", view, cx);
+
+        view.move_right(&MoveRight, cx);
+        view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
+        assert_selection_ranges("use std::ยซห‡sยปtr::{foo, bar}\n\n  {ยซห‡bยปaz.qux()}", view, cx);
+
+        view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
+        assert_selection_ranges("use stdยซห‡::sยปtr::{foo, bar}\n\n  ยซห‡{bยปaz.qux()}", view, cx);
+
+        view.select_to_next_word_end(&SelectToNextWordEnd, cx);
+        assert_selection_ranges("use std::ยซห‡sยปtr::{foo, bar}\n\n  {ยซห‡bยปaz.qux()}", view, cx);
+    });
+}
+
+#[gpui::test]
+fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer =
+                MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+
+    view.update(cx, |view, cx| {
+        view.set_wrap_width(Some(140.), cx);
+        assert_eq!(
+            view.display_text(cx),
+            "use one::{\n    two::three::\n    four::five\n};"
+        );
+
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
+        });
+
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
+        );
+
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
+        );
+
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
+        );
+
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
+        );
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
+        );
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+    let window = cx.window;
+    window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
+
+    cx.set_state(
+        &r#"ห‡one
+        two
+
+        three
+        fourห‡
+        five
+
+        six"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"one
+        two
+        ห‡
+        three
+        four
+        five
+        ห‡
+        six"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"one
+        two
+
+        three
+        four
+        five
+        ห‡
+        sixห‡"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"one
+        two
+
+        three
+        four
+        five
+
+        sixห‡"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"one
+        two
+
+        three
+        four
+        five
+        ห‡
+        six"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"one
+        two
+        ห‡
+        three
+        four
+        five
+
+        six"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"ห‡one
+        two
+
+        three
+        four
+        five
+
+        six"#
+            .unindent(),
+    );
+}
+
+#[gpui::test]
+async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+    let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+    let window = cx.window;
+    window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx);
+
+    cx.set_state(
+        &r#"ห‡one
+        two
+        three
+        four
+        five
+        six
+        seven
+        eight
+        nine
+        ten
+        "#,
+    );
+
+    cx.update_editor(|editor, cx| {
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.));
+        editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
+        editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.));
+        editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
+
+        editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.));
+        editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
+    });
+}
+
+#[gpui::test]
+async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let line_height = cx.update_editor(|editor, cx| {
+        editor.set_vertical_scroll_margin(2, cx);
+        editor.style(cx).text.line_height(cx.font_cache())
+    });
+
+    let window = cx.window;
+    window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx);
+
+    cx.set_state(
+        &r#"ห‡one
+            two
+            three
+            four
+            five
+            six
+            seven
+            eight
+            nine
+            ten
+        "#,
+    );
+    cx.update_editor(|editor, cx| {
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0));
+    });
+
+    // Add a cursor below the visible area. Since both cursors cannot fit
+    // on screen, the editor autoscrolls to reveal the newest cursor, and
+    // allows the vertical scroll margin below that cursor.
+    cx.update_editor(|editor, cx| {
+        editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+            selections.select_ranges([
+                Point::new(0, 0)..Point::new(0, 0),
+                Point::new(6, 0)..Point::new(6, 0),
+            ]);
+        })
+    });
+    cx.update_editor(|editor, cx| {
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
+    });
+
+    // Move down. The editor cursor scrolls down to track the newest cursor.
+    cx.update_editor(|editor, cx| {
+        editor.move_down(&Default::default(), cx);
+    });
+    cx.update_editor(|editor, cx| {
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0));
+    });
+
+    // Add a cursor above the visible area. Since both cursors fit on screen,
+    // the editor scrolls to show both.
+    cx.update_editor(|editor, cx| {
+        editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+            selections.select_ranges([
+                Point::new(1, 0)..Point::new(1, 0),
+                Point::new(6, 0)..Point::new(6, 0),
+            ]);
+        })
+    });
+    cx.update_editor(|editor, cx| {
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0));
+    });
+}
+
+#[gpui::test]
+async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+    let window = cx.window;
+    window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
+
+    cx.set_state(
+        &r#"
+        ห‡one
+        two
+        threeห‡
+        four
+        five
+        six
+        seven
+        eight
+        nine
+        ten
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+        one
+        two
+        three
+        ห‡four
+        five
+        sixห‡
+        seven
+        eight
+        nine
+        ten
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+        one
+        two
+        three
+        four
+        five
+        six
+        ห‡seven
+        eight
+        nineห‡
+        ten
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+        one
+        two
+        three
+        ห‡four
+        five
+        sixห‡
+        seven
+        eight
+        nine
+        ten
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+        ห‡one
+        two
+        threeห‡
+        four
+        five
+        six
+        seven
+        eight
+        nine
+        ten
+        "#
+        .unindent(),
+    );
+
+    // Test select collapsing
+    cx.update_editor(|editor, cx| {
+        editor.move_page_down(&MovePageDown::default(), cx);
+        editor.move_page_down(&MovePageDown::default(), cx);
+        editor.move_page_down(&MovePageDown::default(), cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+        one
+        two
+        three
+        four
+        five
+        six
+        seven
+        eight
+        nine
+        ห‡ten
+        ห‡"#
+        .unindent(),
+    );
+}
+
+#[gpui::test]
+async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state("one ยซtwo threeห‡ยป four");
+    cx.update_editor(|editor, cx| {
+        editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
+        assert_eq!(editor.text(cx), " four");
+    });
+}
+
+#[gpui::test]
+fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("one two three four", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                // an empty selection - the preceding word fragment is deleted
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                // characters selected - they are deleted
+                DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12),
+            ])
+        });
+        view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx);
+        assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four");
+    });
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                // an empty selection - the following word fragment is deleted
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+                // characters selected - they are deleted
+                DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10),
+            ])
+        });
+        view.delete_to_next_word_end(&DeleteToNextWordEnd, cx);
+        assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our");
+    });
+}
+
+#[gpui::test]
+fn test_newline(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+                DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
+            ])
+        });
+
+        view.newline(&Newline, cx);
+        assert_eq!(view.text(cx), "aa\naa\n  \n    bb\n    bb\n");
+    });
+}
+
+#[gpui::test]
+fn test_newline_with_old_selections(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(
+                "
+                a
+                b(
+                    X
+                )
+                c(
+                    X
+                )
+            "
+                .unindent()
+                .as_str(),
+                cx,
+            );
+            let mut editor = build_editor(buffer.clone(), cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([
+                    Point::new(2, 4)..Point::new(2, 5),
+                    Point::new(5, 4)..Point::new(5, 5),
+                ])
+            });
+            editor
+        })
+        .root(cx);
+
+    editor.update(cx, |editor, cx| {
+        // Edit the buffer directly, deleting ranges surrounding the editor's selections
+        editor.buffer.update(cx, |buffer, cx| {
+            buffer.edit(
+                [
+                    (Point::new(1, 2)..Point::new(3, 0), ""),
+                    (Point::new(4, 2)..Point::new(6, 0), ""),
+                ],
+                None,
+                cx,
+            );
+            assert_eq!(
+                buffer.read(cx).text(),
+                "
+                    a
+                    b()
+                    c()
+                "
+                .unindent()
+            );
+        });
+        assert_eq!(
+            editor.selections.ranges(cx),
+            &[
+                Point::new(1, 2)..Point::new(1, 2),
+                Point::new(2, 2)..Point::new(2, 2),
+            ],
+        );
+
+        editor.newline(&Newline, cx);
+        assert_eq!(
+            editor.text(cx),
+            "
+                a
+                b(
+                )
+                c(
+                )
+            "
+            .unindent()
+        );
+
+        // The selections are moved after the inserted newlines
+        assert_eq!(
+            editor.selections.ranges(cx),
+            &[
+                Point::new(2, 0)..Point::new(2, 0),
+                Point::new(4, 0)..Point::new(4, 0),
+            ],
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_newline_above(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
+
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig::default(),
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+        .unwrap(),
+    );
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+    cx.set_state(indoc! {"
+        const a: ห‡A = (
+            (ห‡
+                ยซconst_functionห‡ยป(ห‡),
+                soยซmห‡ยปetยซhห‡ยปing_ห‡else,ห‡
+            )ห‡
+        ห‡);ห‡
+    "});
+
+    cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
+    cx.assert_editor_state(indoc! {"
+        ห‡
+        const a: A = (
+            ห‡
+            (
+                ห‡
+                ห‡
+                const_function(),
+                ห‡
+                ห‡
+                ห‡
+                ห‡
+                something_else,
+                ห‡
+            )
+            ห‡
+            ห‡
+        );
+    "});
+}
+
+#[gpui::test]
+async fn test_newline_below(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
+
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig::default(),
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+        .unwrap(),
+    );
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+    cx.set_state(indoc! {"
+        const a: ห‡A = (
+            (ห‡
+                ยซconst_functionห‡ยป(ห‡),
+                soยซmห‡ยปetยซhห‡ยปing_ห‡else,ห‡
+            )ห‡
+        ห‡);ห‡
+    "});
+
+    cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: A = (
+            ห‡
+            (
+                ห‡
+                const_function(),
+                ห‡
+                ห‡
+                something_else,
+                ห‡
+                ห‡
+                ห‡
+                ห‡
+            )
+            ห‡
+        );
+        ห‡
+        ห‡
+    "});
+}
+
+#[gpui::test]
+async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            line_comment: Some("//".into()),
+            ..LanguageConfig::default()
+        },
+        None,
+    ));
+    {
+        let mut cx = EditorTestContext::new(cx).await;
+        cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+        cx.set_state(indoc! {"
+        // Fooห‡
+    "});
+
+        cx.update_editor(|e, cx| e.newline(&Newline, cx));
+        cx.assert_editor_state(indoc! {"
+        // Foo
+        //ห‡
+    "});
+        // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
+        cx.set_state(indoc! {"
+        ห‡// Foo
+    "});
+        cx.update_editor(|e, cx| e.newline(&Newline, cx));
+        cx.assert_editor_state(indoc! {"
+
+        ห‡// Foo
+    "});
+    }
+    // Ensure that comment continuations can be disabled.
+    update_test_language_settings(cx, |settings| {
+        settings.defaults.extend_comment_on_newline = Some(false);
+    });
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state(indoc! {"
+        // Fooห‡
+    "});
+    cx.update_editor(|e, cx| e.newline(&Newline, cx));
+    cx.assert_editor_state(indoc! {"
+        // Foo
+        ห‡
+    "});
+}
+
+#[gpui::test]
+fn test_insert_with_old_selections(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
+            let mut editor = build_editor(buffer.clone(), cx);
+            editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20]));
+            editor
+        })
+        .root(cx);
+
+    editor.update(cx, |editor, cx| {
+        // Edit the buffer directly, deleting ranges surrounding the editor's selections
+        editor.buffer.update(cx, |buffer, cx| {
+            buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
+            assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
+        });
+        assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
+
+        editor.insert("Z", cx);
+        assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
+
+        // The selections are moved after the inserted characters
+        assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
+    });
+}
+
+#[gpui::test]
+async fn test_tab(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(3)
+    });
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state(indoc! {"
+        ห‡abห‡c
+        ห‡๐Ÿ€ห‡๐Ÿ€ห‡efg
+        dห‡
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+           ห‡ab ห‡c
+           ห‡๐Ÿ€  ห‡๐Ÿ€  ห‡efg
+        d  ห‡
+    "});
+
+    cx.set_state(indoc! {"
+        a
+        ยซ๐Ÿ€ห‡ยป๐Ÿ€ยซ๐Ÿ€ห‡ยป๐Ÿ€ยซ๐Ÿ€ห‡ยป
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        a
+           ยซ๐Ÿ€ห‡ยป๐Ÿ€ยซ๐Ÿ€ห‡ยป๐Ÿ€ยซ๐Ÿ€ห‡ยป
+    "});
+}
+
+#[gpui::test]
+async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig::default(),
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+        .unwrap(),
+    );
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+    // cursors that are already at the suggested indent level insert
+    // a soft tab. cursors that are to the left of the suggested indent
+    // auto-indent their line.
+    cx.set_state(indoc! {"
+        ห‡
+        const a: B = (
+            c(
+                d(
+        ห‡
+                )
+        ห‡
+        ห‡    )
+        );
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+            ห‡
+        const a: B = (
+            c(
+                d(
+                    ห‡
+                )
+                ห‡
+            ห‡)
+        );
+    "});
+
+    // handle auto-indent when there are multiple cursors on the same line
+    cx.set_state(indoc! {"
+        const a: B = (
+            c(
+        ห‡    ห‡
+        ห‡    )
+        );
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: B = (
+            c(
+                ห‡
+            ห‡)
+        );
+    "});
+}
+
+#[gpui::test]
+async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
+
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig::default(),
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
+        .unwrap(),
+    );
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+    cx.set_state(indoc! {"
+        fn a() {
+            if b {
+        \t ห‡c
+            }
+        }
+    "});
+
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            if b {
+                ห‡c
+            }
+        }
+    "});
+}
+
+#[gpui::test]
+async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4);
+    });
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    cx.set_state(indoc! {"
+          ยซoneห‡ยป ยซtwoห‡ยป
+        three
+         four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+            ยซoneห‡ยป ยซtwoห‡ยป
+        three
+         four
+    "});
+
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซoneห‡ยป ยซtwoห‡ยป
+        three
+         four
+    "});
+
+    // select across line ending
+    cx.set_state(indoc! {"
+        one two
+        tยซhree
+        ห‡ยป four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+            tยซhree
+        ห‡ยป four
+    "});
+
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        tยซhree
+        ห‡ยป four
+    "});
+
+    // Ensure that indenting/outdenting works when the cursor is at column 0.
+    cx.set_state(indoc! {"
+        one two
+        ห‡three
+            four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+            ห‡three
+            four
+    "});
+
+    cx.set_state(indoc! {"
+        one two
+        ห‡    three
+            four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        ห‡three
+            four
+    "});
+}
+
+#[gpui::test]
+async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.hard_tabs = Some(true);
+    });
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // select two ranges on one line
+    cx.set_state(indoc! {"
+        ยซoneห‡ยป ยซtwoห‡ยป
+        three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        \tยซoneห‡ยป ยซtwoห‡ยป
+        three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        \t\tยซoneห‡ยป ยซtwoห‡ยป
+        three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        \tยซoneห‡ยป ยซtwoห‡ยป
+        three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซoneห‡ยป ยซtwoห‡ยป
+        three
+        four
+    "});
+
+    // select across a line ending
+    cx.set_state(indoc! {"
+        one two
+        tยซhree
+        ห‡ยปfour
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        \ttยซhree
+        ห‡ยปfour
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        \t\ttยซhree
+        ห‡ยปfour
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        \ttยซhree
+        ห‡ยปfour
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        tยซhree
+        ห‡ยปfour
+    "});
+
+    // Ensure that indenting/outdenting works when the cursor is at column 0.
+    cx.set_state(indoc! {"
+        one two
+        ห‡three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        ห‡three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        \tห‡three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        ห‡three
+        four
+    "});
+}
+
+#[gpui::test]
+fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
+    init_test(cx, |settings| {
+        settings.languages.extend([
+            (
+                "TOML".into(),
+                LanguageSettingsContent {
+                    tab_size: NonZeroU32::new(2),
+                    ..Default::default()
+                },
+            ),
+            (
+                "Rust".into(),
+                LanguageSettingsContent {
+                    tab_size: NonZeroU32::new(4),
+                    ..Default::default()
+                },
+            ),
+        ]);
+    });
+
+    let toml_language = Arc::new(Language::new(
+        LanguageConfig {
+            name: "TOML".into(),
+            ..Default::default()
+        },
+        None,
+    ));
+    let rust_language = Arc::new(Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            ..Default::default()
+        },
+        None,
+    ));
+
+    let toml_buffer = cx.add_model(|cx| {
+        Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx)
+    });
+    let rust_buffer = cx.add_model(|cx| {
+        Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n")
+            .with_language(rust_language, cx)
+    });
+    let multibuffer = cx.add_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        multibuffer.push_excerpts(
+            toml_buffer.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(2, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multibuffer.push_excerpts(
+            rust_buffer.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(1, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multibuffer
+    });
+
+    cx.add_window(|cx| {
+        let mut editor = build_editor(multibuffer, cx);
+
+        assert_eq!(
+            editor.text(cx),
+            indoc! {"
+                a = 1
+                b = 2
+
+                const c: usize = 3;
+            "}
+        );
+
+        select_ranges(
+            &mut editor,
+            indoc! {"
+                ยซaห‡ยป = 1
+                b = 2
+
+                ยซconst c:ห‡ยป usize = 3;
+            "},
+            cx,
+        );
+
+        editor.tab(&Tab, cx);
+        assert_text_with_selections(
+            &mut editor,
+            indoc! {"
+                  ยซaห‡ยป = 1
+                b = 2
+
+                    ยซconst c:ห‡ยป usize = 3;
+            "},
+            cx,
+        );
+        editor.tab_prev(&TabPrev, cx);
+        assert_text_with_selections(
+            &mut editor,
+            indoc! {"
+                ยซaห‡ยป = 1
+                b = 2
+
+                ยซconst c:ห‡ยป usize = 3;
+            "},
+            cx,
+        );
+
+        editor
+    });
+}
+
+#[gpui::test]
+async fn test_backspace(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Basic backspace
+    cx.set_state(indoc! {"
+        onห‡e two three
+        fouยซrห‡ยป five six
+        seven ยซห‡eight nine
+        ยปten
+    "});
+    cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+    cx.assert_editor_state(indoc! {"
+        oห‡e two three
+        fouห‡ five six
+        seven ห‡ten
+    "});
+
+    // Test backspace inside and around indents
+    cx.set_state(indoc! {"
+        zero
+            ห‡one
+                ห‡two
+            ห‡ ห‡ ห‡  three
+        ห‡  ห‡  four
+    "});
+    cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+    cx.assert_editor_state(indoc! {"
+        zero
+        ห‡one
+            ห‡two
+        ห‡  threeห‡  four
+    "});
+
+    // Test backspace with line_mode set to true
+    cx.update_editor(|e, _| e.selections.line_mode = true);
+    cx.set_state(indoc! {"
+        The ห‡quick ห‡brown
+        fox jumps over
+        the lazy dog
+        ห‡The quยซick bห‡ยปrown"});
+    cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+    cx.assert_editor_state(indoc! {"
+        ห‡fox jumps over
+        the lazy dogห‡"});
+}
+
+#[gpui::test]
+async fn test_delete(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state(indoc! {"
+        onห‡e two three
+        fouยซrห‡ยป five six
+        seven ยซห‡eight nine
+        ยปten
+    "});
+    cx.update_editor(|e, cx| e.delete(&Delete, cx));
+    cx.assert_editor_state(indoc! {"
+        onห‡ two three
+        fouห‡ five six
+        seven ห‡ten
+    "});
+
+    // Test backspace with line_mode set to true
+    cx.update_editor(|e, _| e.selections.line_mode = true);
+    cx.set_state(indoc! {"
+        The ห‡quick ห‡brown
+        fox ยซห‡jumยปps over
+        the lazy dog
+        ห‡The quยซick bห‡ยปrown"});
+    cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+    cx.assert_editor_state("ห‡the lazy dogห‡");
+}
+
+#[gpui::test]
+fn test_delete_line(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+            ])
+        });
+        view.delete_line(&DeleteLine, cx);
+        assert_eq!(view.display_text(cx), "ghi");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)
+            ]
+        );
+    });
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)])
+        });
+        view.delete_line(&DeleteLine, cx);
+        assert_eq!(view.display_text(cx), "ghi\n");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
+        let mut editor = build_editor(buffer.clone(), cx);
+        let buffer = buffer.read(cx).as_singleton().unwrap();
+
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 0)..Point::new(0, 0)]
+        );
+
+        // When on single line, replace newline at end by space
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 3)..Point::new(0, 3)]
+        );
+
+        // When multiple lines are selected, remove newlines that are spanned by the selection
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
+        });
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 11)..Point::new(0, 11)]
+        );
+
+        // Undo should be transactional
+        editor.undo(&Undo, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 5)..Point::new(2, 2)]
+        );
+
+        // When joining an empty line don't insert a space
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
+        });
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [Point::new(2, 3)..Point::new(2, 3)]
+        );
+
+        // We can remove trailing newlines
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [Point::new(2, 3)..Point::new(2, 3)]
+        );
+
+        // We don't blow up on the last line
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [Point::new(2, 3)..Point::new(2, 3)]
+        );
+
+        // reset to test indentation
+        editor.buffer.update(cx, |buffer, cx| {
+            buffer.edit(
+                [
+                    (Point::new(1, 0)..Point::new(1, 2), "  "),
+                    (Point::new(2, 0)..Point::new(2, 3), "  \n\td"),
+                ],
+                None,
+                cx,
+            )
+        });
+
+        // We remove any leading spaces
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\n  c\n  \n\td");
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
+        });
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb c\n  \n\td");
+
+        // We don't insert a space for a line containing only spaces
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
+
+        // We ignore any leading tabs
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
+
+        editor
+    });
+}
+
+#[gpui::test]
+fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
+        let mut editor = build_editor(buffer.clone(), cx);
+        let buffer = buffer.read(cx).as_singleton().unwrap();
+
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([
+                Point::new(0, 2)..Point::new(1, 1),
+                Point::new(1, 2)..Point::new(1, 2),
+                Point::new(3, 1)..Point::new(3, 2),
+            ])
+        });
+
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
+
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [
+                Point::new(0, 7)..Point::new(0, 7),
+                Point::new(1, 3)..Point::new(1, 3)
+            ]
+        );
+        editor
+    });
+}
+
+#[gpui::test]
+async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Test sort_lines_case_insensitive()
+    cx.set_state(indoc! {"
+        ยซz
+        y
+        x
+        Z
+        Y
+        Xห‡ยป
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซx
+        X
+        y
+        Y
+        z
+        Zห‡ยป
+    "});
+
+    // Test reverse_lines()
+    cx.set_state(indoc! {"
+        ยซ5
+        4
+        3
+        2
+        1ห‡ยป
+    "});
+    cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซ1
+        2
+        3
+        4
+        5ห‡ยป
+    "});
+
+    // Skip testing shuffle_line()
+
+    // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
+    // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
+
+    // Don't manipulate when cursor is on single line, but expand the selection
+    cx.set_state(indoc! {"
+        ddห‡dd
+        ccc
+        bb
+        a
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซddddห‡ยป
+        ccc
+        bb
+        a
+    "});
+
+    // Basic manipulate case
+    // Start selection moves to column 0
+    // End of selection shrinks to fit shorter line
+    cx.set_state(indoc! {"
+        ddยซd
+        ccc
+        bb
+        aaaaaห‡ยป
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซaaaaa
+        bb
+        ccc
+        dddห‡ยป
+    "});
+
+    // Manipulate case with newlines
+    cx.set_state(indoc! {"
+        ddยซd
+        ccc
+
+        bb
+        aaaaa
+
+        ห‡ยป
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซ
+
+        aaaaa
+        bb
+        ccc
+        dddห‡ยป
+
+    "});
+}
+
+#[gpui::test]
+async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Manipulate with multiple selections on a single line
+    cx.set_state(indoc! {"
+        ddยซdd
+        cห‡ยปcยซc
+        bb
+        aaaห‡ยปaa
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซaaaaa
+        bb
+        ccc
+        ddddห‡ยป
+    "});
+
+    // Manipulate with multiple disjoin selections
+    cx.set_state(indoc! {"
+        5ยซ
+        4
+        3
+        2
+        1ห‡ยป
+
+        ddยซdd
+        ccc
+        bb
+        aaaห‡ยปaa
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซ1
+        2
+        3
+        4
+        5ห‡ยป
+
+        ยซaaaaa
+        bb
+        ccc
+        ddddห‡ยป
+    "});
+}
+
+#[gpui::test]
+async fn test_manipulate_text(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Test convert_to_upper_case()
+    cx.set_state(indoc! {"
+        ยซhello worldห‡ยป
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซHELLO WORLDห‡ยป
+    "});
+
+    // Test convert_to_lower_case()
+    cx.set_state(indoc! {"
+        ยซHELLO WORLDห‡ยป
+    "});
+    cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซhello worldห‡ยป
+    "});
+
+    // Test multiple line, single selection case
+    // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
+    cx.set_state(indoc! {"
+        ยซThe quick brown
+        fox jumps over
+        the lazy dogห‡ยป
+    "});
+    cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซThe Quick Brown
+        Fox Jumps Over
+        The Lazy Dogห‡ยป
+    "});
+
+    // Test multiple line, single selection case
+    // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
+    cx.set_state(indoc! {"
+        ยซThe quick brown
+        fox jumps over
+        the lazy dogห‡ยป
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซTheQuickBrown
+        FoxJumpsOver
+        TheLazyDogห‡ยป
+    "});
+
+    // From here on out, test more complex cases of manipulate_text()
+
+    // Test no selection case - should affect words cursors are in
+    // Cursor at beginning, middle, and end of word
+    cx.set_state(indoc! {"
+        ห‡hello big beauห‡tiful worldห‡
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซHELLOห‡ยป big ยซBEAUTIFULห‡ยป ยซWORLDห‡ยป
+    "});
+
+    // Test multiple selections on a single line and across multiple lines
+    cx.set_state(indoc! {"
+        ยซTheห‡ยป quick ยซbrown
+        foxห‡ยป jumps ยซoverห‡ยป
+        the ยซlazyห‡ยป dog
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซTHEห‡ยป quick ยซBROWN
+        FOXห‡ยป jumps ยซOVERห‡ยป
+        the ยซLAZYห‡ยป dog
+    "});
+
+    // Test case where text length grows
+    cx.set_state(indoc! {"
+        ยซtschรผรŸห‡ยป
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซTSCHรœSSห‡ยป
+    "});
+
+    // Test to make sure we don't crash when text shrinks
+    cx.set_state(indoc! {"
+        aaa_bbbห‡
+    "});
+    cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซaaaBbbห‡ยป
+    "});
+
+    // Test to make sure we all aware of the fact that each word can grow and shrink
+    // Final selections should be aware of this fact
+    cx.set_state(indoc! {"
+        aaa_bห‡bb bbห‡b_ccc ห‡ccc_ddd
+    "});
+    cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซaaaBbbห‡ยป ยซbbbCccห‡ยป ยซcccDddห‡ยป
+    "});
+}
+
+#[gpui::test]
+fn test_duplicate_line(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+                DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+            ])
+        });
+        view.duplicate_line(&DuplicateLine, cx);
+        assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+                DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+                DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
+            ]
+        );
+    });
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
+            ])
+        });
+        view.duplicate_line(&DuplicateLine, cx);
+        assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1),
+                DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1),
+            ]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_move_line_up_down(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.fold_ranges(
+            vec![
+                Point::new(0, 2)..Point::new(1, 2),
+                Point::new(2, 3)..Point::new(4, 1),
+                Point::new(7, 0)..Point::new(8, 4),
+            ],
+            true,
+            cx,
+        );
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
+                DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
+                DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2),
+            ])
+        });
+        assert_eq!(
+            view.display_text(cx),
+            "aaโ‹ฏbbb\ncccโ‹ฏeeee\nfffff\nggggg\nโ‹ฏi\njjjjj"
+        );
+
+        view.move_line_up(&MoveLineUp, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "aaโ‹ฏbbb\ncccโ‹ฏeeee\nggggg\nโ‹ฏi\njjjjj\nfffff"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
+                DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
+                DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.move_line_down(&MoveLineDown, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "cccโ‹ฏeeee\naaโ‹ฏbbb\nfffff\nggggg\nโ‹ฏi\njjjjj"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
+                DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
+                DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.move_line_down(&MoveLineDown, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "cccโ‹ฏeeee\nfffff\naaโ‹ฏbbb\nggggg\nโ‹ฏi\njjjjj"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
+                DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
+                DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
+                DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.move_line_up(&MoveLineUp, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "cccโ‹ฏeeee\naaโ‹ฏbbb\nggggg\nโ‹ฏi\njjjjj\nfffff"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
+                DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
+                DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
+            ]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    editor.update(cx, |editor, cx| {
+        let snapshot = editor.buffer.read(cx).snapshot(cx);
+        editor.insert_blocks(
+            [BlockProperties {
+                style: BlockStyle::Fixed,
+                position: snapshot.anchor_after(Point::new(2, 0)),
+                disposition: BlockDisposition::Below,
+                height: 1,
+                render: Arc::new(|_| Empty::new().into_any()),
+            }],
+            Some(Autoscroll::fit()),
+            cx,
+        );
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
+        });
+        editor.move_line_down(&MoveLineDown, cx);
+    });
+}
+
+#[gpui::test]
+fn test_transpose(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
+
+        editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bac");
+        assert_eq!(editor.selections.ranges(cx), [2..2]);
+
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bca");
+        assert_eq!(editor.selections.ranges(cx), [3..3]);
+
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bac");
+        assert_eq!(editor.selections.ranges(cx), [3..3]);
+
+        editor
+    });
+
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
+
+        editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acb\nde");
+        assert_eq!(editor.selections.ranges(cx), [3..3]);
+
+        editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acbd\ne");
+        assert_eq!(editor.selections.ranges(cx), [5..5]);
+
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acbde\n");
+        assert_eq!(editor.selections.ranges(cx), [6..6]);
+
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acbd\ne");
+        assert_eq!(editor.selections.ranges(cx), [6..6]);
+
+        editor
+    });
+
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
+
+        editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bacd\ne");
+        assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
+
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcade\n");
+        assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
+
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcda\ne");
+        assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
+
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcade\n");
+        assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
+
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcaed\n");
+        assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
+
+        editor
+    });
+
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("๐Ÿ๐Ÿ€โœ‹", cx), cx);
+
+        editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "๐Ÿ€๐Ÿโœ‹");
+        assert_eq!(editor.selections.ranges(cx), [8..8]);
+
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "๐Ÿ€โœ‹๐Ÿ");
+        assert_eq!(editor.selections.ranges(cx), [11..11]);
+
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "๐Ÿ€๐Ÿโœ‹");
+        assert_eq!(editor.selections.ranges(cx), [11..11]);
+
+        editor
+    });
+}
+
+#[gpui::test]
+async fn test_clipboard(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    cx.set_state("ยซoneโœ… ห‡ยปtwo ยซthree ห‡ยปfour ยซfive ห‡ยปsix ");
+    cx.update_editor(|e, cx| e.cut(&Cut, cx));
+    cx.assert_editor_state("ห‡two ห‡four ห‡six ");
+
+    // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
+    cx.set_state("two ห‡four ห‡six ห‡");
+    cx.update_editor(|e, cx| e.paste(&Paste, cx));
+    cx.assert_editor_state("two oneโœ… ห‡four three ห‡six five ห‡");
+
+    // Paste again but with only two cursors. Since the number of cursors doesn't
+    // match the number of slices in the clipboard, the entire clipboard text
+    // is pasted at each cursor.
+    cx.set_state("ห‡two oneโœ… four three six five ห‡");
+    cx.update_editor(|e, cx| {
+        e.handle_input("( ", cx);
+        e.paste(&Paste, cx);
+        e.handle_input(") ", cx);
+    });
+    cx.assert_editor_state(
+        &([
+            "( oneโœ… ",
+            "three ",
+            "five ) ห‡two oneโœ… four three six five ( oneโœ… ",
+            "three ",
+            "five ) ห‡",
+        ]
+        .join("\n")),
+    );
+
+    // Cut with three selections, one of which is full-line.
+    cx.set_state(indoc! {"
+        1ยซ2ห‡ยป3
+        4ห‡567
+        ยซ8ห‡ยป9"});
+    cx.update_editor(|e, cx| e.cut(&Cut, cx));
+    cx.assert_editor_state(indoc! {"
+        1ห‡3
+        ห‡9"});
+
+    // Paste with three selections, noticing how the copied selection that was full-line
+    // gets inserted before the second cursor.
+    cx.set_state(indoc! {"
+        1ห‡3
+        9ห‡
+        ยซoห‡ยปne"});
+    cx.update_editor(|e, cx| e.paste(&Paste, cx));
+    cx.assert_editor_state(indoc! {"
+        12ห‡3
+        4567
+        9ห‡
+        8ห‡ne"});
+
+    // Copy with a single cursor only, which writes the whole line into the clipboard.
+    cx.set_state(indoc! {"
+        The quick brown
+        fox juห‡mps over
+        the lazy dog"});
+    cx.update_editor(|e, cx| e.copy(&Copy, cx));
+    cx.cx.assert_clipboard_content(Some("fox jumps over\n"));
+
+    // Paste with three selections, noticing how the copied full-line selection is inserted
+    // before the empty selections but replaces the selection that is non-empty.
+    cx.set_state(indoc! {"
+        Tห‡he quick brown
+        ยซfoห‡ยปx jumps over
+        tห‡he lazy dog"});
+    cx.update_editor(|e, cx| e.paste(&Paste, cx));
+    cx.assert_editor_state(indoc! {"
+        fox jumps over
+        Tห‡he quick brown
+        fox jumps over
+        ห‡x jumps over
+        fox jumps over
+        tห‡he lazy dog"});
+}
+
+#[gpui::test]
+async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    let language = Arc::new(Language::new(
+        LanguageConfig::default(),
+        Some(tree_sitter_rust::language()),
+    ));
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+    // Cut an indented block, without the leading whitespace.
+    cx.set_state(indoc! {"
+        const a: B = (
+            c(),
+            ยซd(
+                e,
+                f
+            )ห‡ยป
+        );
+    "});
+    cx.update_editor(|e, cx| e.cut(&Cut, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: B = (
+            c(),
+            ห‡
+        );
+    "});
+
+    // Paste it at the same position.
+    cx.update_editor(|e, cx| e.paste(&Paste, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: B = (
+            c(),
+            d(
+                e,
+                f
+            )ห‡
+        );
+    "});
+
+    // Paste it at a line with a lower indent level.
+    cx.set_state(indoc! {"
+        ห‡
+        const a: B = (
+            c(),
+        );
+    "});
+    cx.update_editor(|e, cx| e.paste(&Paste, cx));
+    cx.assert_editor_state(indoc! {"
+        d(
+            e,
+            f
+        )ห‡
+        const a: B = (
+            c(),
+        );
+    "});
+
+    // Cut an indented block, with the leading whitespace.
+    cx.set_state(indoc! {"
+        const a: B = (
+            c(),
+        ยซ    d(
+                e,
+                f
+            )
+        ห‡ยป);
+    "});
+    cx.update_editor(|e, cx| e.cut(&Cut, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: B = (
+            c(),
+        ห‡);
+    "});
+
+    // Paste it at the same position.
+    cx.update_editor(|e, cx| e.paste(&Paste, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: B = (
+            c(),
+            d(
+                e,
+                f
+            )
+        ห‡);
+    "});
+
+    // Paste it at a line with a higher indent level.
+    cx.set_state(indoc! {"
+        const a: B = (
+            c(),
+            d(
+                e,
+                fห‡
+            )
+        );
+    "});
+    cx.update_editor(|e, cx| e.paste(&Paste, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: B = (
+            c(),
+            d(
+                e,
+                f    d(
+                    e,
+                    f
+                )
+        ห‡
+            )
+        );
+    "});
+}
+
+#[gpui::test]
+fn test_select_all(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.select_all(&SelectAll, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_select_line(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+                DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
+            ])
+        });
+        view.select_line(&SelectLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
+                DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.select_line(&SelectLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0),
+                DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.select_line(&SelectLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_split_selection_into_lines(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+    view.update(cx, |view, cx| {
+        view.fold_ranges(
+            vec![
+                Point::new(0, 2)..Point::new(1, 2),
+                Point::new(2, 3)..Point::new(4, 1),
+                Point::new(7, 0)..Point::new(8, 4),
+            ],
+            true,
+            cx,
+        );
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+                DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
+            ])
+        });
+        assert_eq!(view.display_text(cx), "aaโ‹ฏbbb\ncccโ‹ฏeeee\nfffff\nggggg\nโ‹ฏi");
+    });
+
+    view.update(cx, |view, cx| {
+        view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "aaaaa\nbbbbb\ncccโ‹ฏeeee\nfffff\nggggg\nโ‹ฏi"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
+                DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)])
+        });
+        view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
+                DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
+                DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
+                DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
+                DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5),
+                DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5),
+                DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5),
+                DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0)
+            ]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_add_selection_above_below(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)])
+        });
+    });
+    view.update(cx, |view, cx| {
+        view.add_selection_above(&AddSelectionAbove, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.add_selection_above(&AddSelectionAbove, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.add_selection_below(&AddSelectionBelow, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]
+        );
+
+        view.undo_selection(&UndoSelection, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
+            ]
+        );
+
+        view.redo_selection(&RedoSelection, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.add_selection_below(&AddSelectionBelow, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3),
+                DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.add_selection_below(&AddSelectionBelow, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3),
+                DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)])
+        });
+    });
+    view.update(cx, |view, cx| {
+        view.add_selection_below(&AddSelectionBelow, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
+                DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.add_selection_below(&AddSelectionBelow, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
+                DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.add_selection_above(&AddSelectionAbove, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.add_selection_above(&AddSelectionAbove, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)])
+        });
+        view.add_selection_below(&AddSelectionBelow, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
+                DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.add_selection_below(&AddSelectionBelow, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
+                DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
+                DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.add_selection_above(&AddSelectionAbove, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
+                DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)])
+        });
+    });
+    view.update(cx, |view, cx| {
+        view.add_selection_above(&AddSelectionAbove, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1),
+                DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1),
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.add_selection_below(&AddSelectionBelow, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1),
+                DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1),
+            ]
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_select_next(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state("abc\nห‡abc abc\ndefabc\nabc");
+
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("abc\nยซabcห‡ยป abc\ndefabc\nabc");
+
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("abc\nยซabcห‡ยป ยซabcห‡ยป\ndefabc\nabc");
+
+    cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+    cx.assert_editor_state("abc\nยซabcห‡ยป abc\ndefabc\nabc");
+
+    cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+    cx.assert_editor_state("abc\nยซabcห‡ยป ยซabcห‡ยป\ndefabc\nabc");
+
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("abc\nยซabcห‡ยป ยซabcห‡ยป\ndefabc\nยซabcห‡ยป");
+
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("ยซabcห‡ยป\nยซabcห‡ยป ยซabcห‡ยป\ndefabc\nยซabcห‡ยป");
+}
+
+#[gpui::test]
+async fn test_select_previous(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    {
+        // `Select previous` without a selection (selects wordwise)
+        let mut cx = EditorTestContext::new(cx).await;
+        cx.set_state("abc\nห‡abc abc\ndefabc\nabc");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("abc\nยซabcห‡ยป abc\ndefabc\nabc");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("ยซabcห‡ยป\nยซabcห‡ยป abc\ndefabc\nabc");
+
+        cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+        cx.assert_editor_state("abc\nยซabcห‡ยป abc\ndefabc\nabc");
+
+        cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+        cx.assert_editor_state("ยซabcห‡ยป\nยซabcห‡ยป abc\ndefabc\nabc");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("ยซabcห‡ยป\nยซabcห‡ยป abc\ndefabc\nยซabcห‡ยป");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("ยซabcห‡ยป\nยซabcห‡ยป ยซabcห‡ยป\ndefabc\nยซabcห‡ยป");
+    }
+    {
+        // `Select previous` with a selection
+        let mut cx = EditorTestContext::new(cx).await;
+        cx.set_state("abc\nยซห‡abcยป abc\ndefabc\nabc");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("ยซabcห‡ยป\nยซห‡abcยป abc\ndefabc\nabc");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("ยซabcห‡ยป\nยซห‡abcยป abc\ndefabc\nยซabcห‡ยป");
+
+        cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+        cx.assert_editor_state("ยซabcห‡ยป\nยซห‡abcยป abc\ndefabc\nabc");
+
+        cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+        cx.assert_editor_state("ยซabcห‡ยป\nยซห‡abcยป abc\ndefabc\nยซabcห‡ยป");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("ยซabcห‡ยป\nยซห‡abcยป abc\ndefยซabcห‡ยป\nยซabcห‡ยป");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("ยซabcห‡ยป\nยซห‡abcยป ยซabcห‡ยป\ndefยซabcห‡ยป\nยซabcห‡ยป");
+    }
+}
+
+#[gpui::test]
+async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(Language::new(
+        LanguageConfig::default(),
+        Some(tree_sitter_rust::language()),
+    ));
+
+    let text = r#"
+        use mod1::mod2::{mod3, mod4};
+
+        fn fn_1(param1: bool, param2: &str) {
+            let var1 = "text";
+        }
+    "#
+    .unindent();
+
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+                DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+                DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+            ]);
+        });
+        view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
+        &[
+            DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
+            DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+            DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
+        ]
+    );
+
+    view.update(cx, |view, cx| {
+        view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+            DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
+        ]
+    );
+
+    view.update(cx, |view, cx| {
+        view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
+    );
+
+    // Trying to expand the selected syntax node one more time has no effect.
+    view.update(cx, |view, cx| {
+        view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
+    );
+
+    view.update(cx, |view, cx| {
+        view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+            DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
+        ]
+    );
+
+    view.update(cx, |view, cx| {
+        view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
+            DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+            DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
+        ]
+    );
+
+    view.update(cx, |view, cx| {
+        view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+            DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+            DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+        ]
+    );
+
+    // Trying to shrink the selected syntax node one more time has no effect.
+    view.update(cx, |view, cx| {
+        view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+            DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+            DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+        ]
+    );
+
+    // Ensure that we keep expanding the selection if the larger selection starts or ends within
+    // a fold.
+    view.update(cx, |view, cx| {
+        view.fold_ranges(
+            vec![
+                Point::new(0, 21)..Point::new(0, 24),
+                Point::new(3, 20)..Point::new(3, 22),
+            ],
+            true,
+            cx,
+        );
+        view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+            DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+            DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23),
+        ]
+    );
+}
+
+#[gpui::test]
+async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".to_string(),
+                            end: "}".to_string(),
+                            close: false,
+                            newline: true,
+                        },
+                        BracketPair {
+                            start: "(".to_string(),
+                            end: ")".to_string(),
+                            close: false,
+                            newline: true,
+                        },
+                    ],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(
+            r#"
+                (_ "(" ")" @end) @indent
+                (_ "{" "}" @end) @indent
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let text = "fn a() {}";
+
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    editor
+        .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    editor.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
+        editor.newline(&Newline, cx);
+        assert_eq!(editor.text(cx), "fn a(\n    \n) {\n    \n}\n");
+        assert_eq!(
+            editor.selections.ranges(cx),
+            &[
+                Point::new(1, 4)..Point::new(1, 4),
+                Point::new(3, 4)..Point::new(3, 4),
+                Point::new(5, 0)..Point::new(5, 0)
+            ]
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            brackets: BracketPairConfig {
+                pairs: vec![
+                    BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "(".to_string(),
+                        end: ")".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "/*".to_string(),
+                        end: " */".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "[".to_string(),
+                        end: "]".to_string(),
+                        close: false,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "\"".to_string(),
+                        end: "\"".to_string(),
+                        close: true,
+                        newline: false,
+                    },
+                ],
+                ..Default::default()
+            },
+            autoclose_before: "})]".to_string(),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(language.clone());
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(language), cx);
+    });
+
+    cx.set_state(
+        &r#"
+            ๐Ÿ€ห‡
+            ฮตห‡
+            โค๏ธห‡
+        "#
+        .unindent(),
+    );
+
+    // autoclose multiple nested brackets at multiple cursors
+    cx.update_editor(|view, cx| {
+        view.handle_input("{", cx);
+        view.handle_input("{", cx);
+        view.handle_input("{", cx);
+    });
+    cx.assert_editor_state(
+        &"
+            ๐Ÿ€{{{ห‡}}}
+            ฮต{{{ห‡}}}
+            โค๏ธ{{{ห‡}}}
+        "
+        .unindent(),
+    );
+
+    // insert a different closing bracket
+    cx.update_editor(|view, cx| {
+        view.handle_input(")", cx);
+    });
+    cx.assert_editor_state(
+        &"
+            ๐Ÿ€{{{)ห‡}}}
+            ฮต{{{)ห‡}}}
+            โค๏ธ{{{)ห‡}}}
+        "
+        .unindent(),
+    );
+
+    // skip over the auto-closed brackets when typing a closing bracket
+    cx.update_editor(|view, cx| {
+        view.move_right(&MoveRight, cx);
+        view.handle_input("}", cx);
+        view.handle_input("}", cx);
+        view.handle_input("}", cx);
+    });
+    cx.assert_editor_state(
+        &"
+            ๐Ÿ€{{{)}}}}ห‡
+            ฮต{{{)}}}}ห‡
+            โค๏ธ{{{)}}}}ห‡
+        "
+        .unindent(),
+    );
+
+    // autoclose multi-character pairs
+    cx.set_state(
+        &"
+            ห‡
+            ห‡
+        "
+        .unindent(),
+    );
+    cx.update_editor(|view, cx| {
+        view.handle_input("/", cx);
+        view.handle_input("*", cx);
+    });
+    cx.assert_editor_state(
+        &"
+            /*ห‡ */
+            /*ห‡ */
+        "
+        .unindent(),
+    );
+
+    // one cursor autocloses a multi-character pair, one cursor
+    // does not autoclose.
+    cx.set_state(
+        &"
+            /ห‡
+            ห‡
+        "
+        .unindent(),
+    );
+    cx.update_editor(|view, cx| view.handle_input("*", cx));
+    cx.assert_editor_state(
+        &"
+            /*ห‡ */
+            *ห‡
+        "
+        .unindent(),
+    );
+
+    // Don't autoclose if the next character isn't whitespace and isn't
+    // listed in the language's "autoclose_before" section.
+    cx.set_state("ห‡a b");
+    cx.update_editor(|view, cx| view.handle_input("{", cx));
+    cx.assert_editor_state("{ห‡a b");
+
+    // Don't autoclose if `close` is false for the bracket pair
+    cx.set_state("ห‡");
+    cx.update_editor(|view, cx| view.handle_input("[", cx));
+    cx.assert_editor_state("[ห‡");
+
+    // Surround with brackets if text is selected
+    cx.set_state("ยซaห‡ยป b");
+    cx.update_editor(|view, cx| view.handle_input("{", cx));
+    cx.assert_editor_state("{ยซaห‡ยป} b");
+
+    // Autclose pair where the start and end characters are the same
+    cx.set_state("aห‡");
+    cx.update_editor(|view, cx| view.handle_input("\"", cx));
+    cx.assert_editor_state("a\"ห‡\"");
+    cx.update_editor(|view, cx| view.handle_input("\"", cx));
+    cx.assert_editor_state("a\"\"ห‡");
+}
+
+#[gpui::test]
+async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let html_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "HTML".into(),
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "<".into(),
+                            end: ">".into(),
+                            close: true,
+                            ..Default::default()
+                        },
+                        BracketPair {
+                            start: "{".into(),
+                            end: "}".into(),
+                            close: true,
+                            ..Default::default()
+                        },
+                        BracketPair {
+                            start: "(".into(),
+                            end: ")".into(),
+                            close: true,
+                            ..Default::default()
+                        },
+                    ],
+                    ..Default::default()
+                },
+                autoclose_before: "})]>".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_html::language()),
+        )
+        .with_injection_query(
+            r#"
+            (script_element
+                (raw_text) @content
+                (#set! "language" "javascript"))
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let javascript_language = Arc::new(Language::new(
+        LanguageConfig {
+            name: "JavaScript".into(),
+            brackets: BracketPairConfig {
+                pairs: vec![
+                    BracketPair {
+                        start: "/*".into(),
+                        end: " */".into(),
+                        close: true,
+                        ..Default::default()
+                    },
+                    BracketPair {
+                        start: "{".into(),
+                        end: "}".into(),
+                        close: true,
+                        ..Default::default()
+                    },
+                    BracketPair {
+                        start: "(".into(),
+                        end: ")".into(),
+                        close: true,
+                        ..Default::default()
+                    },
+                ],
+                ..Default::default()
+            },
+            autoclose_before: "})]>".into(),
+            ..Default::default()
+        },
+        Some(tree_sitter_typescript::language_tsx()),
+    ));
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(html_language.clone());
+    registry.add(javascript_language.clone());
+
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(html_language), cx);
+    });
+
+    cx.set_state(
+        &r#"
+            <body>ห‡
+                <script>
+                    var x = 1;ห‡
+                </script>
+            </body>ห‡
+        "#
+        .unindent(),
+    );
+
+    // Precondition: different languages are active at different locations.
+    cx.update_editor(|editor, cx| {
+        let snapshot = editor.snapshot(cx);
+        let cursors = editor.selections.ranges::<usize>(cx);
+        let languages = cursors
+            .iter()
+            .map(|c| snapshot.language_at(c.start).unwrap().name())
+            .collect::<Vec<_>>();
+        assert_eq!(
+            languages,
+            &["HTML".into(), "JavaScript".into(), "HTML".into()]
+        );
+    });
+
+    // Angle brackets autoclose in HTML, but not JavaScript.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("<", cx);
+        editor.handle_input("a", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body><aห‡>
+                <script>
+                    var x = 1;<aห‡
+                </script>
+            </body><aห‡>
+        "#
+        .unindent(),
+    );
+
+    // Curly braces and parens autoclose in both HTML and JavaScript.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input(" b=", cx);
+        editor.handle_input("{", cx);
+        editor.handle_input("c", cx);
+        editor.handle_input("(", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body><a b={c(ห‡)}>
+                <script>
+                    var x = 1;<a b={c(ห‡)}
+                </script>
+            </body><a b={c(ห‡)}>
+        "#
+        .unindent(),
+    );
+
+    // Brackets that were already autoclosed are skipped.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input(")", cx);
+        editor.handle_input("d", cx);
+        editor.handle_input("}", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body><a b={c()d}ห‡>
+                <script>
+                    var x = 1;<a b={c()d}ห‡
+                </script>
+            </body><a b={c()d}ห‡>
+        "#
+        .unindent(),
+    );
+    cx.update_editor(|editor, cx| {
+        editor.handle_input(">", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body><a b={c()d}>ห‡
+                <script>
+                    var x = 1;<a b={c()d}>ห‡
+                </script>
+            </body><a b={c()d}>ห‡
+        "#
+        .unindent(),
+    );
+
+    // Reset
+    cx.set_state(
+        &r#"
+            <body>ห‡
+                <script>
+                    var x = 1;ห‡
+                </script>
+            </body>ห‡
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("<", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body><ห‡>
+                <script>
+                    var x = 1;<ห‡
+                </script>
+            </body><ห‡>
+        "#
+        .unindent(),
+    );
+
+    // When backspacing, the closing angle brackets are removed.
+    cx.update_editor(|editor, cx| {
+        editor.backspace(&Backspace, cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body>ห‡
+                <script>
+                    var x = 1;ห‡
+                </script>
+            </body>ห‡
+        "#
+        .unindent(),
+    );
+
+    // Block comments autoclose in JavaScript, but not HTML.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("/", cx);
+        editor.handle_input("*", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body>/*ห‡
+                <script>
+                    var x = 1;/*ห‡ */
+                </script>
+            </body>/*ห‡
+        "#
+        .unindent(),
+    );
+}
+
+#[gpui::test]
+async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let rust_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                brackets: serde_json::from_value(json!([
+                    { "start": "{", "end": "}", "close": true, "newline": true },
+                    { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
+                ]))
+                .unwrap(),
+                autoclose_before: "})]>".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_override_query("(string_literal) @string")
+        .unwrap(),
+    );
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(rust_language.clone());
+
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(rust_language), cx);
+    });
+
+    cx.set_state(
+        &r#"
+            let x = ห‡
+        "#
+        .unindent(),
+    );
+
+    // Inserting a quotation mark. A closing quotation mark is automatically inserted.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = "ห‡"
+        "#
+        .unindent(),
+    );
+
+    // Inserting another quotation mark. The cursor moves across the existing
+    // automatically-inserted quotation mark.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = ""ห‡
+        "#
+        .unindent(),
+    );
+
+    // Reset
+    cx.set_state(
+        &r#"
+            let x = ห‡
+        "#
+        .unindent(),
+    );
+
+    // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("\"", cx);
+        editor.handle_input(" ", cx);
+        editor.move_left(&Default::default(), cx);
+        editor.handle_input("\\", cx);
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = "\"ห‡ "
+        "#
+        .unindent(),
+    );
+
+    // Inserting a closing quotation mark at the position of an automatically-inserted quotation
+    // mark. Nothing is inserted.
+    cx.update_editor(|editor, cx| {
+        editor.move_right(&Default::default(), cx);
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = "\" "ห‡
+        "#
+        .unindent(),
+    );
+}
+
+#[gpui::test]
+async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            brackets: BracketPairConfig {
+                pairs: vec![
+                    BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "/* ".to_string(),
+                        end: "*/".to_string(),
+                        close: true,
+                        ..Default::default()
+                    },
+                ],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+
+    let text = r#"
+        a
+        b
+        c
+    "#
+    .unindent();
+
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
+            ])
+        });
+
+        view.handle_input("{", cx);
+        view.handle_input("{", cx);
+        view.handle_input("{", cx);
+        assert_eq!(
+            view.text(cx),
+            "
+                {{{a}}}
+                {{{b}}}
+                {{{c}}}
+            "
+            .unindent()
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4),
+                DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4),
+                DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4)
+            ]
+        );
+
+        view.undo(&Undo, cx);
+        view.undo(&Undo, cx);
+        view.undo(&Undo, cx);
+        assert_eq!(
+            view.text(cx),
+            "
+                a
+                b
+                c
+            "
+            .unindent()
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
+            ]
+        );
+
+        // Ensure inserting the first character of a multi-byte bracket pair
+        // doesn't surround the selections with the bracket.
+        view.handle_input("/", cx);
+        assert_eq!(
+            view.text(cx),
+            "
+                /
+                /
+                /
+            "
+            .unindent()
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
+            ]
+        );
+
+        view.undo(&Undo, cx);
+        assert_eq!(
+            view.text(cx),
+            "
+                a
+                b
+                c
+            "
+            .unindent()
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
+            ]
+        );
+
+        // Ensure inserting the last character of a multi-byte bracket pair
+        // doesn't surround the selections with the bracket.
+        view.handle_input("*", cx);
+        assert_eq!(
+            view.text(cx),
+            "
+                *
+                *
+                *
+            "
+            .unindent()
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
+            ]
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            brackets: BracketPairConfig {
+                pairs: vec![BracketPair {
+                    start: "{".to_string(),
+                    end: "}".to_string(),
+                    close: true,
+                    newline: true,
+                }],
+                ..Default::default()
+            },
+            autoclose_before: "}".to_string(),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+
+    let text = r#"
+        a
+        b
+        c
+    "#
+    .unindent();
+
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    editor
+        .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    editor.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([
+                Point::new(0, 1)..Point::new(0, 1),
+                Point::new(1, 1)..Point::new(1, 1),
+                Point::new(2, 1)..Point::new(2, 1),
+            ])
+        });
+
+        editor.handle_input("{", cx);
+        editor.handle_input("{", cx);
+        editor.handle_input("_", cx);
+        assert_eq!(
+            editor.text(cx),
+            "
+                a{{_}}
+                b{{_}}
+                c{{_}}
+            "
+            .unindent()
+        );
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [
+                Point::new(0, 4)..Point::new(0, 4),
+                Point::new(1, 4)..Point::new(1, 4),
+                Point::new(2, 4)..Point::new(2, 4)
+            ]
+        );
+
+        editor.backspace(&Default::default(), cx);
+        editor.backspace(&Default::default(), cx);
+        assert_eq!(
+            editor.text(cx),
+            "
+                a{}
+                b{}
+                c{}
+            "
+            .unindent()
+        );
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [
+                Point::new(0, 2)..Point::new(0, 2),
+                Point::new(1, 2)..Point::new(1, 2),
+                Point::new(2, 2)..Point::new(2, 2)
+            ]
+        );
+
+        editor.delete_to_previous_word_start(&Default::default(), cx);
+        assert_eq!(
+            editor.text(cx),
+            "
+                a
+                b
+                c
+            "
+            .unindent()
+        );
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [
+                Point::new(0, 1)..Point::new(0, 1),
+                Point::new(1, 1)..Point::new(1, 1),
+                Point::new(2, 1)..Point::new(2, 1)
+            ]
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_snippets(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let (text, insertion_ranges) = marked_text_ranges(
+        indoc! {"
+            a.ห‡ b
+            a.ห‡ b
+            a.ห‡ b
+        "},
+        false,
+    );
+
+    let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+
+    editor.update(cx, |editor, cx| {
+        let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
+
+        editor
+            .insert_snippet(&insertion_ranges, snippet, cx)
+            .unwrap();
+
+        fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
+            let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
+            assert_eq!(editor.text(cx), expected_text);
+            assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
+        }
+
+        assert(
+            editor,
+            cx,
+            indoc! {"
+                a.f(ยซoneยป, two, ยซthreeยป) b
+                a.f(ยซoneยป, two, ยซthreeยป) b
+                a.f(ยซoneยป, two, ยซthreeยป) b
+            "},
+        );
+
+        // Can't move earlier than the first tab stop
+        assert!(!editor.move_to_prev_snippet_tabstop(cx));
+        assert(
+            editor,
+            cx,
+            indoc! {"
+                a.f(ยซoneยป, two, ยซthreeยป) b
+                a.f(ยซoneยป, two, ยซthreeยป) b
+                a.f(ยซoneยป, two, ยซthreeยป) b
+            "},
+        );
+
+        assert!(editor.move_to_next_snippet_tabstop(cx));
+        assert(
+            editor,
+            cx,
+            indoc! {"
+                a.f(one, ยซtwoยป, three) b
+                a.f(one, ยซtwoยป, three) b
+                a.f(one, ยซtwoยป, three) b
+            "},
+        );
+
+        editor.move_to_prev_snippet_tabstop(cx);
+        assert(
+            editor,
+            cx,
+            indoc! {"
+                a.f(ยซoneยป, two, ยซthreeยป) b
+                a.f(ยซoneยป, two, ยซthreeยป) b
+                a.f(ยซoneยป, two, ยซthreeยป) b
+            "},
+        );
+
+        assert!(editor.move_to_next_snippet_tabstop(cx));
+        assert(
+            editor,
+            cx,
+            indoc! {"
+                a.f(one, ยซtwoยป, three) b
+                a.f(one, ยซtwoยป, three) b
+                a.f(one, ยซtwoยป, three) b
+            "},
+        );
+        assert!(editor.move_to_next_snippet_tabstop(cx));
+        assert(
+            editor,
+            cx,
+            indoc! {"
+                a.f(one, two, three)ห‡ b
+                a.f(one, two, three)ห‡ b
+                a.f(one, two, three)ห‡ b
+            "},
+        );
+
+        // As soon as the last tab stop is reached, snippet state is gone
+        editor.move_to_prev_snippet_tabstop(cx);
+        assert(
+            editor,
+            cx,
+            indoc! {"
+                a.f(one, two, three)ห‡ b
+                a.f(one, two, three)ห‡ b
+                a.f(one, two, three)ห‡ b
+            "},
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                document_formatting_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+        .await
+        .unwrap();
+
+    cx.foreground().start_waiting();
+    let fake_server = fake_servers.next().await.unwrap();
+
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+    let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+    fake_server
+        .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            assert_eq!(params.options.tab_size, 4);
+            Ok(Some(vec![lsp::TextEdit::new(
+                lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+                ", ".to_string(),
+            )]))
+        })
+        .next()
+        .await;
+    cx.foreground().start_waiting();
+    save.await.unwrap();
+    assert_eq!(
+        editor.read_with(cx, |editor, cx| editor.text(cx)),
+        "one, two\nthree\n"
+    );
+    assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+    // Ensure we can still save even if formatting hangs.
+    fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+        assert_eq!(
+            params.text_document.uri,
+            lsp::Url::from_file_path("/file.rs").unwrap()
+        );
+        futures::future::pending::<()>().await;
+        unreachable!()
+    });
+    let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+    cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
+    cx.foreground().start_waiting();
+    save.await.unwrap();
+    assert_eq!(
+        editor.read_with(cx, |editor, cx| editor.text(cx)),
+        "one\ntwo\nthree\n"
+    );
+    assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+    // Set rust language override and assert overridden tabsize is sent to language server
+    update_test_language_settings(cx, |settings| {
+        settings.languages.insert(
+            "Rust".into(),
+            LanguageSettingsContent {
+                tab_size: NonZeroU32::new(8),
+                ..Default::default()
+            },
+        );
+    });
+
+    let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+    fake_server
+        .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            assert_eq!(params.options.tab_size, 8);
+            Ok(Some(vec![]))
+        })
+        .next()
+        .await;
+    cx.foreground().start_waiting();
+    save.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+        .await
+        .unwrap();
+
+    cx.foreground().start_waiting();
+    let fake_server = fake_servers.next().await.unwrap();
+
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+    let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+    fake_server
+        .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            assert_eq!(params.options.tab_size, 4);
+            Ok(Some(vec![lsp::TextEdit::new(
+                lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+                ", ".to_string(),
+            )]))
+        })
+        .next()
+        .await;
+    cx.foreground().start_waiting();
+    save.await.unwrap();
+    assert_eq!(
+        editor.read_with(cx, |editor, cx| editor.text(cx)),
+        "one, two\nthree\n"
+    );
+    assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+    // Ensure we can still save even if formatting hangs.
+    fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
+        move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            futures::future::pending::<()>().await;
+            unreachable!()
+        },
+    );
+    let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+    cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
+    cx.foreground().start_waiting();
+    save.await.unwrap();
+    assert_eq!(
+        editor.read_with(cx, |editor, cx| editor.text(cx)),
+        "one\ntwo\nthree\n"
+    );
+    assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+    // Set rust language override and assert overridden tabsize is sent to language server
+    update_test_language_settings(cx, |settings| {
+        settings.languages.insert(
+            "Rust".into(),
+            LanguageSettingsContent {
+                tab_size: NonZeroU32::new(8),
+                ..Default::default()
+            },
+        );
+    });
+
+    let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+    fake_server
+        .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            assert_eq!(params.options.tab_size, 8);
+            Ok(Some(vec![]))
+        })
+        .next()
+        .await;
+    cx.foreground().start_waiting();
+    save.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
+    });
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            // Enable Prettier formatting for the same buffer, and ensure
+            // LSP is called instead of Prettier.
+            prettier_parser_name: Some("test_parser".to_string()),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                document_formatting_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    project.update(cx, |project, _| {
+        project.languages().add(Arc::new(language));
+    });
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+        .await
+        .unwrap();
+
+    cx.foreground().start_waiting();
+    let fake_server = fake_servers.next().await.unwrap();
+
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+
+    let format = editor.update(cx, |editor, cx| {
+        editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+    });
+    fake_server
+        .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            assert_eq!(params.options.tab_size, 4);
+            Ok(Some(vec![lsp::TextEdit::new(
+                lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+                ", ".to_string(),
+            )]))
+        })
+        .next()
+        .await;
+    cx.foreground().start_waiting();
+    format.await.unwrap();
+    assert_eq!(
+        editor.read_with(cx, |editor, cx| editor.text(cx)),
+        "one, two\nthree\n"
+    );
+
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    // Ensure we don't lock if formatting hangs.
+    fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+        assert_eq!(
+            params.text_document.uri,
+            lsp::Url::from_file_path("/file.rs").unwrap()
+        );
+        futures::future::pending::<()>().await;
+        unreachable!()
+    });
+    let format = editor.update(cx, |editor, cx| {
+        editor.perform_format(project, FormatTrigger::Manual, cx)
+    });
+    cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
+    cx.foreground().start_waiting();
+    format.await.unwrap();
+    assert_eq!(
+        editor.read_with(cx, |editor, cx| editor.text(cx)),
+        "one\ntwo\nthree\n"
+    );
+}
+
+#[gpui::test]
+async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            document_formatting_provider: Some(lsp::OneOf::Left(true)),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.set_state(indoc! {"
+        one.twoห‡
+    "});
+
+    // The format request takes a long time. When it completes, it inserts
+    // a newline and an indent before the `.`
+    cx.lsp
+        .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
+            let executor = cx.background();
+            async move {
+                executor.timer(Duration::from_millis(100)).await;
+                Ok(Some(vec![lsp::TextEdit {
+                    range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
+                    new_text: "\n    ".into(),
+                }]))
+            }
+        });
+
+    // Submit a format request.
+    let format_1 = cx
+        .update_editor(|editor, cx| editor.format(&Format, cx))
+        .unwrap();
+    cx.foreground().run_until_parked();
+
+    // Submit a second format request.
+    let format_2 = cx
+        .update_editor(|editor, cx| editor.format(&Format, cx))
+        .unwrap();
+    cx.foreground().run_until_parked();
+
+    // Wait for both format requests to complete
+    cx.foreground().advance_clock(Duration::from_millis(200));
+    cx.foreground().start_waiting();
+    format_1.await.unwrap();
+    cx.foreground().start_waiting();
+    format_2.await.unwrap();
+
+    // The formatting edits only happens once.
+    cx.assert_editor_state(indoc! {"
+        one
+            .twoห‡
+    "});
+}
+
+#[gpui::test]
+async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.formatter = Some(language_settings::Formatter::Auto)
+    });
+
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            document_formatting_provider: Some(lsp::OneOf::Left(true)),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    // Set up a buffer white some trailing whitespace and no trailing newline.
+    cx.set_state(
+        &[
+            "one ",   //
+            "twoห‡",   //
+            "three ", //
+            "four",   //
+        ]
+        .join("\n"),
+    );
+
+    // Submit a format request.
+    let format = cx
+        .update_editor(|editor, cx| editor.format(&Format, cx))
+        .unwrap();
+
+    // Record which buffer changes have been sent to the language server
+    let buffer_changes = Arc::new(Mutex::new(Vec::new()));
+    cx.lsp
+        .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
+            let buffer_changes = buffer_changes.clone();
+            move |params, _| {
+                buffer_changes.lock().extend(
+                    params
+                        .content_changes
+                        .into_iter()
+                        .map(|e| (e.range.unwrap(), e.text)),
+                );
+            }
+        });
+
+    // Handle formatting requests to the language server.
+    cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
+        let buffer_changes = buffer_changes.clone();
+        move |_, _| {
+            // When formatting is requested, trailing whitespace has already been stripped,
+            // and the trailing newline has already been added.
+            assert_eq!(
+                &buffer_changes.lock()[1..],
+                &[
+                    (
+                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
+                        "".into()
+                    ),
+                    (
+                        lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
+                        "".into()
+                    ),
+                    (
+                        lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
+                        "\n".into()
+                    ),
+                ]
+            );
+
+            // Insert blank lines between each line of the buffer.
+            async move {
+                Ok(Some(vec![
+                    lsp::TextEdit {
+                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
+                        new_text: "\n".into(),
+                    },
+                    lsp::TextEdit {
+                        range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
+                        new_text: "\n".into(),
+                    },
+                ]))
+            }
+        }
+    });
+
+    // After formatting the buffer, the trailing whitespace is stripped,
+    // a newline is appended, and the edits provided by the language server
+    // have been applied.
+    format.await.unwrap();
+    cx.assert_editor_state(
+        &[
+            "one",   //
+            "",      //
+            "twoห‡",  //
+            "",      //
+            "three", //
+            "four",  //
+            "",      //
+        ]
+        .join("\n"),
+    );
+
+    // Undoing the formatting undoes the trailing whitespace removal, the
+    // trailing newline, and the LSP edits.
+    cx.update_buffer(|buffer, cx| buffer.undo(cx));
+    cx.assert_editor_state(
+        &[
+            "one ",   //
+            "twoห‡",   //
+            "three ", //
+            "four",   //
+        ]
+        .join("\n"),
+    );
+}
+
+#[gpui::test]
+async fn test_completion(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+                resolve_provider: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.set_state(indoc! {"
+        oneห‡
+        two
+        three
+    "});
+    cx.simulate_keystroke(".");
+    handle_completion_request(
+        &mut cx,
+        indoc! {"
+            one.|<>
+            two
+            three
+        "},
+        vec!["first_completion", "second_completion"],
+    )
+    .await;
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+    let apply_additional_edits = cx.update_editor(|editor, cx| {
+        editor.context_menu_next(&Default::default(), cx);
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .unwrap()
+    });
+    cx.assert_editor_state(indoc! {"
+        one.second_completionห‡
+        two
+        three
+    "});
+
+    handle_resolve_completion_request(
+        &mut cx,
+        Some(vec![
+            (
+                //This overlaps with the primary completion edit which is
+                //misbehavior from the LSP spec, test that we filter it out
+                indoc! {"
+                    one.second_ห‡completion
+                    two
+                    threeห‡
+                "},
+                "overlapping additional edit",
+            ),
+            (
+                indoc! {"
+                    one.second_completion
+                    two
+                    threeห‡
+                "},
+                "\nadditional edit",
+            ),
+        ]),
+    )
+    .await;
+    apply_additional_edits.await.unwrap();
+    cx.assert_editor_state(indoc! {"
+        one.second_completionห‡
+        two
+        three
+        additional edit
+    "});
+
+    cx.set_state(indoc! {"
+        one.second_completion
+        twoห‡
+        threeห‡
+        additional edit
+    "});
+    cx.simulate_keystroke(" ");
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+    cx.simulate_keystroke("s");
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+
+    cx.assert_editor_state(indoc! {"
+        one.second_completion
+        two sห‡
+        three sห‡
+        additional edit
+    "});
+    handle_completion_request(
+        &mut cx,
+        indoc! {"
+            one.second_completion
+            two s
+            three <s|>
+            additional edit
+        "},
+        vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+    )
+    .await;
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+
+    cx.simulate_keystroke("i");
+
+    handle_completion_request(
+        &mut cx,
+        indoc! {"
+            one.second_completion
+            two si
+            three <si|>
+            additional edit
+        "},
+        vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+    )
+    .await;
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+
+    let apply_additional_edits = cx.update_editor(|editor, cx| {
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .unwrap()
+    });
+    cx.assert_editor_state(indoc! {"
+        one.second_completion
+        two sixth_completionห‡
+        three sixth_completionห‡
+        additional edit
+    "});
+
+    handle_resolve_completion_request(&mut cx, None).await;
+    apply_additional_edits.await.unwrap();
+
+    cx.update(|cx| {
+        cx.update_global::<SettingsStore, _, _>(|settings, cx| {
+            settings.update_user_settings::<EditorSettings>(cx, |settings| {
+                settings.show_completions_on_input = Some(false);
+            });
+        })
+    });
+    cx.set_state("editorห‡");
+    cx.simulate_keystroke(".");
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+    cx.simulate_keystroke("c");
+    cx.simulate_keystroke("l");
+    cx.simulate_keystroke("o");
+    cx.assert_editor_state("editor.cloห‡");
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+    cx.update_editor(|editor, cx| {
+        editor.show_completions(&ShowCompletions, cx);
+    });
+    handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+    let apply_additional_edits = cx.update_editor(|editor, cx| {
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .unwrap()
+    });
+    cx.assert_editor_state("editor.closeห‡");
+    handle_resolve_completion_request(&mut cx, None).await;
+    apply_additional_edits.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            line_comment: Some("// ".into()),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+    // If multiple selections intersect a line, the line is only toggled once.
+    cx.set_state(indoc! {"
+        fn a() {
+            ยซ//b();
+            ห‡ยป// ยซc();
+            //ห‡ยป  d();
+        }
+    "});
+
+    cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            ยซb();
+            c();
+            ห‡ยป d();
+        }
+    "});
+
+    // The comment prefix is inserted at the same column for every line in a
+    // selection.
+    cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            // ยซb();
+            // c();
+            ห‡ยป//  d();
+        }
+    "});
+
+    // If a selection ends at the beginning of a line, that line is not toggled.
+    cx.set_selections_state(indoc! {"
+        fn a() {
+            // b();
+            ยซ// c();
+        ห‡ยป    //  d();
+        }
+    "});
+
+    cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            // b();
+            ยซc();
+        ห‡ยป    //  d();
+        }
+    "});
+
+    // If a selection span a single line and is empty, the line is toggled.
+    cx.set_state(indoc! {"
+        fn a() {
+            a();
+            b();
+        ห‡
+        }
+    "});
+
+    cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            a();
+            b();
+        //โ€ขห‡
+        }
+    "});
+
+    // If a selection span multiple lines, empty lines are not toggled.
+    cx.set_state(indoc! {"
+        fn a() {
+            ยซa();
+
+            c();ห‡ยป
+        }
+    "});
+
+    cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            // ยซa();
+
+            // c();ห‡ยป
+        }
+    "});
+}
+
+#[gpui::test]
+async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            line_comment: Some("// ".into()),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(language.clone());
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(language), cx);
+    });
+
+    let toggle_comments = &ToggleComments {
+        advance_downwards: true,
+    };
+
+    // Single cursor on one line -> advance
+    // Cursor moves horizontally 3 characters as well on non-blank line
+    cx.set_state(indoc!(
+        "fn a() {
+             ห‡dog();
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+             catห‡();
+        }"
+    ));
+
+    // Single selection on one line -> don't advance
+    cx.set_state(indoc!(
+        "fn a() {
+             ยซdog()ห‡ยป;
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // ยซdog()ห‡ยป;
+             cat();
+        }"
+    ));
+
+    // Multiple cursors on one line -> advance
+    cx.set_state(indoc!(
+        "fn a() {
+             ห‡dห‡og();
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+             catห‡(ห‡);
+        }"
+    ));
+
+    // Multiple cursors on one line, with selection -> don't advance
+    cx.set_state(indoc!(
+        "fn a() {
+             ห‡dห‡ogยซ()ห‡ยป;
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // ห‡dห‡ogยซ()ห‡ยป;
+             cat();
+        }"
+    ));
+
+    // Single cursor on one line -> advance
+    // Cursor moves to column 0 on blank line
+    cx.set_state(indoc!(
+        "fn a() {
+             ห‡dog();
+
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+        ห‡
+             cat();
+        }"
+    ));
+
+    // Single cursor on one line -> advance
+    // Cursor starts and ends at column 0
+    cx.set_state(indoc!(
+        "fn a() {
+         ห‡    dog();
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+         ห‡    cat();
+        }"
+    ));
+}
+
+#[gpui::test]
+async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let html_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "HTML".into(),
+                block_comment: Some(("<!-- ".into(), " -->".into())),
+                ..Default::default()
+            },
+            Some(tree_sitter_html::language()),
+        )
+        .with_injection_query(
+            r#"
+            (script_element
+                (raw_text) @content
+                (#set! "language" "javascript"))
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let javascript_language = Arc::new(Language::new(
+        LanguageConfig {
+            name: "JavaScript".into(),
+            line_comment: Some("// ".into()),
+            ..Default::default()
+        },
+        Some(tree_sitter_typescript::language_tsx()),
+    ));
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(html_language.clone());
+    registry.add(javascript_language.clone());
+
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(html_language), cx);
+    });
+
+    // Toggle comments for empty selections
+    cx.set_state(
+        &r#"
+            <p>A</p>ห‡
+            <p>B</p>ห‡
+            <p>C</p>ห‡
+        "#
+        .unindent(),
+    );
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+            <!-- <p>A</p>ห‡ -->
+            <!-- <p>B</p>ห‡ -->
+            <!-- <p>C</p>ห‡ -->
+        "#
+        .unindent(),
+    );
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+            <p>A</p>ห‡
+            <p>B</p>ห‡
+            <p>C</p>ห‡
+        "#
+        .unindent(),
+    );
+
+    // Toggle comments for mixture of empty and non-empty selections, where
+    // multiple selections occupy a given line.
+    cx.set_state(
+        &r#"
+            <p>Aยซ</p>
+            <p>ห‡ยปB</p>ห‡
+            <p>Cยซ</p>
+            <p>ห‡ยปD</p>ห‡
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+            <!-- <p>Aยซ</p>
+            <p>ห‡ยปB</p>ห‡ -->
+            <!-- <p>Cยซ</p>
+            <p>ห‡ยปD</p>ห‡ -->
+        "#
+        .unindent(),
+    );
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+            <p>Aยซ</p>
+            <p>ห‡ยปB</p>ห‡
+            <p>Cยซ</p>
+            <p>ห‡ยปD</p>ห‡
+        "#
+        .unindent(),
+    );
+
+    // Toggle comments when different languages are active for different
+    // selections.
+    cx.set_state(
+        &r#"
+            ห‡<script>
+                ห‡var x = new Y();
+            ห‡</script>
+        "#
+        .unindent(),
+    );
+    cx.foreground().run_until_parked();
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+            <!-- ห‡<script> -->
+                // ห‡var x = new Y();
+            <!-- ห‡</script> -->
+        "#
+        .unindent(),
+    );
+}
+
+#[gpui::test]
+fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
+    let multibuffer = cx.add_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        multibuffer.push_excerpts(
+            buffer.clone(),
+            [
+                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,
+        );
+        assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
+        multibuffer
+    });
+
+    let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
+    view.update(cx, |view, cx| {
+        assert_eq!(view.text(cx), "aaaa\nbbbb");
+        view.change_selections(None, cx, |s| {
+            s.select_ranges([
+                Point::new(0, 0)..Point::new(0, 0),
+                Point::new(1, 0)..Point::new(1, 0),
+            ])
+        });
+
+        view.handle_input("X", cx);
+        assert_eq!(view.text(cx), "Xaaaa\nXbbbb");
+        assert_eq!(
+            view.selections.ranges(cx),
+            [
+                Point::new(0, 1)..Point::new(0, 1),
+                Point::new(1, 1)..Point::new(1, 1),
+            ]
+        );
+
+        // Ensure the cursor's head is respected when deleting across an excerpt boundary.
+        view.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
+        });
+        view.backspace(&Default::default(), cx);
+        assert_eq!(view.text(cx), "Xa\nbbb");
+        assert_eq!(
+            view.selections.ranges(cx),
+            [Point::new(1, 0)..Point::new(1, 0)]
+        );
+
+        view.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
+        });
+        view.backspace(&Default::default(), cx);
+        assert_eq!(view.text(cx), "X\nbb");
+        assert_eq!(
+            view.selections.ranges(cx),
+            [Point::new(0, 1)..Point::new(0, 1)]
+        );
+    });
+}
+
+#[gpui::test]
+fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let markers = vec![('[', ']').into(), ('(', ')').into()];
+    let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
+        indoc! {"
+            [aaaa
+            (bbbb]
+            cccc)",
+        },
+        markers.clone(),
+    );
+    let excerpt_ranges = markers.into_iter().map(|marker| {
+        let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
+        ExcerptRange {
+            context,
+            primary: None,
+        }
+    });
+    let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text));
+    let multibuffer = cx.add_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
+        multibuffer
+    });
+
+    let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
+    view.update(cx, |view, cx| {
+        let (expected_text, selection_ranges) = marked_text_ranges(
+            indoc! {"
+                aaaa
+                bห‡bbb
+                bห‡bbห‡b
+                cccc"
+            },
+            true,
+        );
+        assert_eq!(view.text(cx), expected_text);
+        view.change_selections(None, cx, |s| s.select_ranges(selection_ranges));
+
+        view.handle_input("X", cx);
+
+        let (expected_text, expected_selections) = marked_text_ranges(
+            indoc! {"
+                aaaa
+                bXห‡bbXb
+                bXห‡bbXห‡b
+                cccc"
+            },
+            false,
+        );
+        assert_eq!(view.text(cx), expected_text);
+        assert_eq!(view.selections.ranges(cx), expected_selections);
+
+        view.newline(&Newline, cx);
+        let (expected_text, expected_selections) = marked_text_ranges(
+            indoc! {"
+                aaaa
+                bX
+                ห‡bbX
+                b
+                bX
+                ห‡bbX
+                ห‡b
+                cccc"
+            },
+            false,
+        );
+        assert_eq!(view.text(cx), expected_text);
+        assert_eq!(view.selections.ranges(cx), expected_selections);
+    });
+}
+
+#[gpui::test]
+fn test_refresh_selections(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
+    let mut excerpt1_id = None;
+    let multibuffer = cx.add_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        excerpt1_id = multibuffer
+            .push_excerpts(
+                buffer.clone(),
+                [
+                    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,
+            )
+            .into_iter()
+            .next();
+        assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
+        multibuffer
+    });
+
+    let editor = cx
+        .add_window(|cx| {
+            let mut editor = build_editor(multibuffer.clone(), cx);
+            let snapshot = editor.snapshot(cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
+            });
+            editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
+            assert_eq!(
+                editor.selections.ranges(cx),
+                [
+                    Point::new(1, 3)..Point::new(1, 3),
+                    Point::new(2, 1)..Point::new(2, 1),
+                ]
+            );
+            editor
+        })
+        .root(cx);
+
+    // Refreshing selections is a no-op when excerpts haven't changed.
+    editor.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.refresh());
+        assert_eq!(
+            editor.selections.ranges(cx),
+            [
+                Point::new(1, 3)..Point::new(1, 3),
+                Point::new(2, 1)..Point::new(2, 1),
+            ]
+        );
+    });
+
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
+    });
+    editor.update(cx, |editor, cx| {
+        // Removing an excerpt causes the first selection to become degenerate.
+        assert_eq!(
+            editor.selections.ranges(cx),
+            [
+                Point::new(0, 0)..Point::new(0, 0),
+                Point::new(0, 1)..Point::new(0, 1)
+            ]
+        );
+
+        // Refreshing selections will relocate the first selection to the original buffer
+        // location.
+        editor.change_selections(None, cx, |s| s.refresh());
+        assert_eq!(
+            editor.selections.ranges(cx),
+            [
+                Point::new(0, 1)..Point::new(0, 1),
+                Point::new(0, 3)..Point::new(0, 3)
+            ]
+        );
+        assert!(editor.selections.pending_anchor().is_some());
+    });
+}
+
+#[gpui::test]
+fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
+    let mut excerpt1_id = None;
+    let multibuffer = cx.add_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        excerpt1_id = multibuffer
+            .push_excerpts(
+                buffer.clone(),
+                [
+                    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,
+            )
+            .into_iter()
+            .next();
+        assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
+        multibuffer
+    });
+
+    let editor = cx
+        .add_window(|cx| {
+            let mut editor = build_editor(multibuffer.clone(), cx);
+            let snapshot = editor.snapshot(cx);
+            editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
+            assert_eq!(
+                editor.selections.ranges(cx),
+                [Point::new(1, 3)..Point::new(1, 3)]
+            );
+            editor
+        })
+        .root(cx);
+
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
+    });
+    editor.update(cx, |editor, cx| {
+        assert_eq!(
+            editor.selections.ranges(cx),
+            [Point::new(0, 0)..Point::new(0, 0)]
+        );
+
+        // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
+        editor.change_selections(None, cx, |s| s.refresh());
+        assert_eq!(
+            editor.selections.ranges(cx),
+            [Point::new(0, 3)..Point::new(0, 3)]
+        );
+        assert!(editor.selections.pending_anchor().is_some());
+    });
+}
+
+#[gpui::test]
+async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".to_string(),
+                            end: "}".to_string(),
+                            close: true,
+                            newline: true,
+                        },
+                        BracketPair {
+                            start: "/* ".to_string(),
+                            end: " */".to_string(),
+                            close: true,
+                            newline: true,
+                        },
+                    ],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query("")
+        .unwrap(),
+    );
+
+    let text = concat!(
+        "{   }\n",     //
+        "  x\n",       //
+        "  /*   */\n", //
+        "x\n",         //
+        "{{} }\n",     //
+    );
+
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
+                DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
+            ])
+        });
+        view.newline(&Newline, cx);
+
+        assert_eq!(
+            view.buffer().read(cx).read(cx).text(),
+            concat!(
+                "{ \n",    // Suppress rustfmt
+                "\n",      //
+                "}\n",     //
+                "  x\n",   //
+                "  /* \n", //
+                "  \n",    //
+                "  */\n",  //
+                "x\n",     //
+                "{{} \n",  //
+                "}\n",     //
+            )
+        );
+    });
+}
+
+#[gpui::test]
+fn test_highlighted_ranges(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
+
+    editor.update(cx, |editor, cx| {
+        struct Type1;
+        struct Type2;
+
+        let buffer = editor.buffer.read(cx).snapshot(cx);
+
+        let anchor_range =
+            |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
+
+        editor.highlight_background::<Type1>(
+            vec![
+                anchor_range(Point::new(2, 1)..Point::new(2, 3)),
+                anchor_range(Point::new(4, 2)..Point::new(4, 4)),
+                anchor_range(Point::new(6, 3)..Point::new(6, 5)),
+                anchor_range(Point::new(8, 4)..Point::new(8, 6)),
+            ],
+            |_| Color::red(),
+            cx,
+        );
+        editor.highlight_background::<Type2>(
+            vec![
+                anchor_range(Point::new(3, 2)..Point::new(3, 5)),
+                anchor_range(Point::new(5, 3)..Point::new(5, 6)),
+                anchor_range(Point::new(7, 4)..Point::new(7, 7)),
+                anchor_range(Point::new(9, 5)..Point::new(9, 8)),
+            ],
+            |_| Color::green(),
+            cx,
+        );
+
+        let snapshot = editor.snapshot(cx);
+        let mut highlighted_ranges = editor.background_highlights_in_range(
+            anchor_range(Point::new(3, 4)..Point::new(7, 4)),
+            &snapshot,
+            theme::current(cx).as_ref(),
+        );
+        // Enforce a consistent ordering based on color without relying on the ordering of the
+        // highlight's `TypeId` which is non-deterministic.
+        highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
+        assert_eq!(
+            highlighted_ranges,
+            &[
+                (
+                    DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5),
+                    Color::green(),
+                ),
+                (
+                    DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6),
+                    Color::green(),
+                ),
+                (
+                    DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
+                    Color::red(),
+                ),
+                (
+                    DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
+                    Color::red(),
+                ),
+            ]
+        );
+        assert_eq!(
+            editor.background_highlights_in_range(
+                anchor_range(Point::new(5, 6)..Point::new(6, 4)),
+                &snapshot,
+                theme::current(cx).as_ref(),
+            ),
+            &[(
+                DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
+                Color::red(),
+            )]
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_following(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let fs = FakeFs::new(cx.background());
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+
+    let buffer = project.update(cx, |project, cx| {
+        let buffer = project
+            .create_buffer(&sample_text(16, 8, 'a'), None, cx)
+            .unwrap();
+        cx.add_model(|cx| MultiBuffer::singleton(buffer, cx))
+    });
+    let leader = cx
+        .add_window(|cx| build_editor(buffer.clone(), cx))
+        .root(cx);
+    let follower = cx
+        .update(|cx| {
+            cx.add_window(
+                WindowOptions {
+                    bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
+                    ..Default::default()
+                },
+                |cx| build_editor(buffer.clone(), cx),
+            )
+        })
+        .root(cx);
+
+    let is_still_following = Rc::new(RefCell::new(true));
+    let follower_edit_event_count = Rc::new(RefCell::new(0));
+    let pending_update = Rc::new(RefCell::new(None));
+    follower.update(cx, {
+        let update = pending_update.clone();
+        let is_still_following = is_still_following.clone();
+        let follower_edit_event_count = follower_edit_event_count.clone();
+        |_, cx| {
+            cx.subscribe(&leader, move |_, leader, event, cx| {
+                leader
+                    .read(cx)
+                    .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
+            })
+            .detach();
+
+            cx.subscribe(&follower, move |_, _, event, cx| {
+                if Editor::should_unfollow_on_event(event, cx) {
+                    *is_still_following.borrow_mut() = false;
+                }
+                if let Event::BufferEdited = event {
+                    *follower_edit_event_count.borrow_mut() += 1;
+                }
+            })
+            .detach();
+        }
+    });
+
+    // Update the selections only
+    leader.update(cx, |leader, cx| {
+        leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
+    });
+    follower
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    follower.read_with(cx, |follower, cx| {
+        assert_eq!(follower.selections.ranges(cx), vec![1..1]);
+    });
+    assert_eq!(*is_still_following.borrow(), true);
+    assert_eq!(*follower_edit_event_count.borrow(), 0);
+
+    // Update the scroll position only
+    leader.update(cx, |leader, cx| {
+        leader.set_scroll_position(vec2f(1.5, 3.5), cx);
+    });
+    follower
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        follower.update(cx, |follower, cx| follower.scroll_position(cx)),
+        vec2f(1.5, 3.5)
+    );
+    assert_eq!(*is_still_following.borrow(), true);
+    assert_eq!(*follower_edit_event_count.borrow(), 0);
+
+    // Update the selections and scroll position. The follower's scroll position is updated
+    // via autoscroll, not via the leader's exact scroll position.
+    leader.update(cx, |leader, cx| {
+        leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
+        leader.request_autoscroll(Autoscroll::newest(), cx);
+        leader.set_scroll_position(vec2f(1.5, 3.5), cx);
+    });
+    follower
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    follower.update(cx, |follower, cx| {
+        assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0));
+        assert_eq!(follower.selections.ranges(cx), vec![0..0]);
+    });
+    assert_eq!(*is_still_following.borrow(), true);
+
+    // Creating a pending selection that precedes another selection
+    leader.update(cx, |leader, cx| {
+        leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
+        leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
+    });
+    follower
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    follower.read_with(cx, |follower, cx| {
+        assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
+    });
+    assert_eq!(*is_still_following.borrow(), true);
+
+    // Extend the pending selection so that it surrounds another selection
+    leader.update(cx, |leader, cx| {
+        leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
+    });
+    follower
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    follower.read_with(cx, |follower, cx| {
+        assert_eq!(follower.selections.ranges(cx), vec![0..2]);
+    });
+
+    // Scrolling locally breaks the follow
+    follower.update(cx, |follower, cx| {
+        let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
+        follower.set_scroll_anchor(
+            ScrollAnchor {
+                anchor: top_anchor,
+                offset: vec2f(0.0, 0.5),
+            },
+            cx,
+        );
+    });
+    assert_eq!(*is_still_following.borrow(), false);
+}
+
+#[gpui::test]
+async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let fs = FakeFs::new(cx.background());
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    let workspace = cx
+        .add_window(|cx| Workspace::test_new(project.clone(), cx))
+        .root(cx);
+    let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+    let leader = pane.update(cx, |_, cx| {
+        let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+        cx.add_view(|cx| build_editor(multibuffer.clone(), cx))
+    });
+
+    // Start following the editor when it has no excerpts.
+    let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
+    let follower_1 = cx
+        .update(|cx| {
+            Editor::from_state_proto(
+                pane.clone(),
+                workspace.clone(),
+                ViewId {
+                    creator: Default::default(),
+                    id: 0,
+                },
+                &mut state_message,
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
+
+    let update_message = Rc::new(RefCell::new(None));
+    follower_1.update(cx, {
+        let update = update_message.clone();
+        |_, cx| {
+            cx.subscribe(&leader, move |_, leader, event, cx| {
+                leader
+                    .read(cx)
+                    .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
+            })
+            .detach();
+        }
+    });
+
+    let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
+        (
+            project
+                .create_buffer("abc\ndef\nghi\njkl\n", None, cx)
+                .unwrap(),
+            project
+                .create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
+                .unwrap(),
+        )
+    });
+
+    // Insert some excerpts.
+    leader.update(cx, |leader, cx| {
+        leader.buffer.update(cx, |multibuffer, cx| {
+            let excerpt_ids = multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    ExcerptRange {
+                        context: 1..6,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 12..15,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 0..3,
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer.insert_excerpts_after(
+                excerpt_ids[0],
+                buffer_2.clone(),
+                [
+                    ExcerptRange {
+                        context: 8..12,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 0..6,
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+        });
+    });
+
+    // Apply the update of adding the excerpts.
+    follower_1
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        follower_1.read_with(cx, |editor, cx| editor.text(cx)),
+        leader.read_with(cx, |editor, cx| editor.text(cx))
+    );
+    update_message.borrow_mut().take();
+
+    // Start following separately after it already has excerpts.
+    let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
+    let follower_2 = cx
+        .update(|cx| {
+            Editor::from_state_proto(
+                pane.clone(),
+                workspace.clone(),
+                ViewId {
+                    creator: Default::default(),
+                    id: 0,
+                },
+                &mut state_message,
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
+    assert_eq!(
+        follower_2.read_with(cx, |editor, cx| editor.text(cx)),
+        leader.read_with(cx, |editor, cx| editor.text(cx))
+    );
+
+    // Remove some excerpts.
+    leader.update(cx, |leader, cx| {
+        leader.buffer.update(cx, |multibuffer, cx| {
+            let excerpt_ids = multibuffer.excerpt_ids();
+            multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
+            multibuffer.remove_excerpts([excerpt_ids[0]], cx);
+        });
+    });
+
+    // Apply the update of removing the excerpts.
+    follower_1
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    follower_2
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    update_message.borrow_mut().take();
+    assert_eq!(
+        follower_1.read_with(cx, |editor, cx| editor.text(cx)),
+        leader.read_with(cx, |editor, cx| editor.text(cx))
+    );
+}
+
+#[test]
+fn test_combine_syntax_and_fuzzy_match_highlights() {
+    let string = "abcdefghijklmnop";
+    let syntax_ranges = [
+        (
+            0..3,
+            HighlightStyle {
+                color: Some(Color::red()),
+                ..Default::default()
+            },
+        ),
+        (
+            4..8,
+            HighlightStyle {
+                color: Some(Color::green()),
+                ..Default::default()
+            },
+        ),
+    ];
+    let match_indices = [4, 6, 7, 8];
+    assert_eq!(
+        combine_syntax_and_fuzzy_match_highlights(
+            string,
+            Default::default(),
+            syntax_ranges.into_iter(),
+            &match_indices,
+        ),
+        &[
+            (
+                0..3,
+                HighlightStyle {
+                    color: Some(Color::red()),
+                    ..Default::default()
+                },
+            ),
+            (
+                4..5,
+                HighlightStyle {
+                    color: Some(Color::green()),
+                    weight: Some(fonts::Weight::BOLD),
+                    ..Default::default()
+                },
+            ),
+            (
+                5..6,
+                HighlightStyle {
+                    color: Some(Color::green()),
+                    ..Default::default()
+                },
+            ),
+            (
+                6..8,
+                HighlightStyle {
+                    color: Some(Color::green()),
+                    weight: Some(fonts::Weight::BOLD),
+                    ..Default::default()
+                },
+            ),
+            (
+                8..9,
+                HighlightStyle {
+                    weight: Some(fonts::Weight::BOLD),
+                    ..Default::default()
+                },
+            ),
+        ]
+    );
+}
+
+#[gpui::test]
+async fn go_to_prev_overlapping_diagnostic(
+    deterministic: Arc<Deterministic>,
+    cx: &mut gpui::TestAppContext,
+) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
+
+    cx.set_state(indoc! {"
+        ห‡fn func(abc def: i32) -> u32 {
+        }
+    "});
+
+    cx.update(|cx| {
+        project.update(cx, |project, cx| {
+            project
+                .update_diagnostics(
+                    LanguageServerId(0),
+                    lsp::PublishDiagnosticsParams {
+                        uri: lsp::Url::from_file_path("/root/file").unwrap(),
+                        version: None,
+                        diagnostics: vec![
+                            lsp::Diagnostic {
+                                range: lsp::Range::new(
+                                    lsp::Position::new(0, 11),
+                                    lsp::Position::new(0, 12),
+                                ),
+                                severity: Some(lsp::DiagnosticSeverity::ERROR),
+                                ..Default::default()
+                            },
+                            lsp::Diagnostic {
+                                range: lsp::Range::new(
+                                    lsp::Position::new(0, 12),
+                                    lsp::Position::new(0, 15),
+                                ),
+                                severity: Some(lsp::DiagnosticSeverity::ERROR),
+                                ..Default::default()
+                            },
+                            lsp::Diagnostic {
+                                range: lsp::Range::new(
+                                    lsp::Position::new(0, 25),
+                                    lsp::Position::new(0, 28),
+                                ),
+                                severity: Some(lsp::DiagnosticSeverity::ERROR),
+                                ..Default::default()
+                            },
+                        ],
+                    },
+                    &[],
+                    cx,
+                )
+                .unwrap()
+        });
+    });
+
+    deterministic.run_until_parked();
+
+    cx.update_editor(|editor, cx| {
+        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+    });
+
+    cx.assert_editor_state(indoc! {"
+        fn func(abc def: i32) -> ห‡u32 {
+        }
+    "});
+
+    cx.update_editor(|editor, cx| {
+        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+    });
+
+    cx.assert_editor_state(indoc! {"
+        fn func(abc ห‡def: i32) -> u32 {
+        }
+    "});
+
+    cx.update_editor(|editor, cx| {
+        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+    });
+
+    cx.assert_editor_state(indoc! {"
+        fn func(abcห‡ def: i32) -> u32 {
+        }
+    "});
+
+    cx.update_editor(|editor, cx| {
+        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+    });
+
+    cx.assert_editor_state(indoc! {"
+        fn func(abc def: i32) -> ห‡u32 {
+        }
+    "});
+}
+
+#[gpui::test]
+async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let diff_base = r#"
+        use some::mod;
+
+        const A: u32 = 42;
+
+        fn main() {
+            println!("hello");
+
+            println!("world");
+        }
+        "#
+    .unindent();
+
+    // Edits are modified, removed, modified, added
+    cx.set_state(
+        &r#"
+        use some::modified;
+
+        ห‡
+        fn main() {
+            println!("hello there");
+
+            println!("around the");
+            println!("world");
+        }
+        "#
+        .unindent(),
+    );
+
+    cx.set_diff_base(Some(&diff_base));
+    deterministic.run_until_parked();
+
+    cx.update_editor(|editor, cx| {
+        //Wrap around the bottom of the buffer
+        for _ in 0..3 {
+            editor.go_to_hunk(&GoToHunk, cx);
+        }
+    });
+
+    cx.assert_editor_state(
+        &r#"
+        ห‡use some::modified;
+
+
+        fn main() {
+            println!("hello there");
+
+            println!("around the");
+            println!("world");
+        }
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| {
+        //Wrap around the top of the buffer
+        for _ in 0..2 {
+            editor.go_to_prev_hunk(&GoToPrevHunk, cx);
+        }
+    });
+
+    cx.assert_editor_state(
+        &r#"
+        use some::modified;
+
+
+        fn main() {
+        ห‡    println!("hello there");
+
+            println!("around the");
+            println!("world");
+        }
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| {
+        editor.go_to_prev_hunk(&GoToPrevHunk, cx);
+    });
+
+    cx.assert_editor_state(
+        &r#"
+        use some::modified;
+
+        ห‡
+        fn main() {
+            println!("hello there");
+
+            println!("around the");
+            println!("world");
+        }
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| {
+        for _ in 0..3 {
+            editor.go_to_prev_hunk(&GoToPrevHunk, cx);
+        }
+    });
+
+    cx.assert_editor_state(
+        &r#"
+        use some::modified;
+
+
+        fn main() {
+        ห‡    println!("hello there");
+
+            println!("around the");
+            println!("world");
+        }
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| {
+        editor.fold(&Fold, cx);
+
+        //Make sure that the fold only gets one hunk
+        for _ in 0..4 {
+            editor.go_to_hunk(&GoToHunk, cx);
+        }
+    });
+
+    cx.assert_editor_state(
+        &r#"
+        ห‡use some::modified;
+
+
+        fn main() {
+            println!("hello there");
+
+            println!("around the");
+            println!("world");
+        }
+        "#
+        .unindent(),
+    );
+}
+
+#[test]
+fn test_split_words() {
+    fn split<'a>(text: &'a str) -> Vec<&'a str> {
+        split_words(text).collect()
+    }
+
+    assert_eq!(split("HelloWorld"), &["Hello", "World"]);
+    assert_eq!(split("hello_world"), &["hello_", "world"]);
+    assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
+    assert_eq!(split("Hello_World"), &["Hello_", "World"]);
+    assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
+    assert_eq!(split("helloworld"), &["helloworld"]);
+}
+
+#[gpui::test]
+async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
+    let mut assert = |before, after| {
+        let _state_context = cx.set_state(before);
+        cx.update_editor(|editor, cx| {
+            editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
+        });
+        cx.assert_editor_state(after);
+    };
+
+    // Outside bracket jumps to outside of matching bracket
+    assert("console.logห‡(var);", "console.log(var)ห‡;");
+    assert("console.log(var)ห‡;", "console.logห‡(var);");
+
+    // Inside bracket jumps to inside of matching bracket
+    assert("console.log(ห‡var);", "console.log(varห‡);");
+    assert("console.log(varห‡);", "console.log(ห‡var);");
+
+    // When outside a bracket and inside, favor jumping to the inside bracket
+    assert(
+        "console.log('foo', [1, 2, 3]ห‡);",
+        "console.log(ห‡'foo', [1, 2, 3]);",
+    );
+    assert(
+        "console.log(ห‡'foo', [1, 2, 3]);",
+        "console.log('foo', [1, 2, 3]ห‡);",
+    );
+
+    // Bias forward if two options are equally likely
+    assert(
+        "let result = curried_fun()ห‡();",
+        "let result = curried_fun()()ห‡;",
+    );
+
+    // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
+    assert(
+        indoc! {"
+            function test() {
+                console.log('test')ห‡
+            }"},
+        indoc! {"
+            function test() {
+                console.logห‡('test')
+            }"},
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let (copilot, copilot_lsp) = Copilot::fake(cx);
+    cx.update(|cx| cx.set_global(copilot));
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    // When inserting, ensure autocompletion is favored over Copilot suggestions.
+    cx.set_state(indoc! {"
+        oneห‡
+        two
+        three
+    "});
+    cx.simulate_keystroke(".");
+    let _ = handle_completion_request(
+        &mut cx,
+        indoc! {"
+            one.|<>
+            two
+            three
+        "},
+        vec!["completion_a", "completion_b"],
+    );
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "one.copilot1".into(),
+            range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    cx.update_editor(|editor, cx| {
+        assert!(editor.context_menu_visible());
+        assert!(!editor.has_active_copilot_suggestion(cx));
+
+        // Confirming a completion inserts it and hides the context menu, without showing
+        // the copilot suggestion afterwards.
+        editor
+            .confirm_completion(&Default::default(), cx)
+            .unwrap()
+            .detach();
+        assert!(!editor.context_menu_visible());
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
+        assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
+    });
+
+    // Ensure Copilot suggestions are shown right away if no autocompletion is available.
+    cx.set_state(indoc! {"
+        oneห‡
+        two
+        three
+    "});
+    cx.simulate_keystroke(".");
+    let _ = handle_completion_request(
+        &mut cx,
+        indoc! {"
+            one.|<>
+            two
+            three
+        "},
+        vec![],
+    );
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "one.copilot1".into(),
+            range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    cx.update_editor(|editor, cx| {
+        assert!(!editor.context_menu_visible());
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
+    });
+
+    // Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
+    cx.set_state(indoc! {"
+        oneห‡
+        two
+        three
+    "});
+    cx.simulate_keystroke(".");
+    let _ = handle_completion_request(
+        &mut cx,
+        indoc! {"
+            one.|<>
+            two
+            three
+        "},
+        vec!["completion_a", "completion_b"],
+    );
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "one.copilot1".into(),
+            range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    cx.update_editor(|editor, cx| {
+        assert!(editor.context_menu_visible());
+        assert!(!editor.has_active_copilot_suggestion(cx));
+
+        // When hiding the context menu, the Copilot suggestion becomes visible.
+        editor.hide_context_menu(cx);
+        assert!(!editor.context_menu_visible());
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
+    });
+
+    // Ensure existing completion is interpolated when inserting again.
+    cx.simulate_keystroke("c");
+    deterministic.run_until_parked();
+    cx.update_editor(|editor, cx| {
+        assert!(!editor.context_menu_visible());
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+    });
+
+    // After debouncing, new Copilot completions should be requested.
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "one.copilot2".into(),
+            range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    cx.update_editor(|editor, cx| {
+        assert!(!editor.context_menu_visible());
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+
+        // Canceling should remove the active Copilot suggestion.
+        editor.cancel(&Default::default(), cx);
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+
+        // After canceling, tabbing shouldn't insert the previously shown suggestion.
+        editor.tab(&Default::default(), cx);
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.c   \ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.c   \ntwo\nthree\n");
+
+        // When undoing the previously active suggestion is shown again.
+        editor.undo(&Default::default(), cx);
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+    });
+
+    // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
+    cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
+    cx.update_editor(|editor, cx| {
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
+
+        // Tabbing when there is an active suggestion inserts it.
+        editor.tab(&Default::default(), cx);
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
+
+        // When undoing the previously active suggestion is shown again.
+        editor.undo(&Default::default(), cx);
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
+
+        // Hide suggestion.
+        editor.cancel(&Default::default(), cx);
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
+    });
+
+    // If an edit occurs outside of this editor but no suggestion is being shown,
+    // we won't make it visible.
+    cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
+    cx.update_editor(|editor, cx| {
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
+        assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
+    });
+
+    // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
+    cx.update_editor(|editor, cx| {
+        editor.set_text("fn foo() {\n  \n}", cx);
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
+        });
+    });
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "    let x = 4;".into(),
+            range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+
+    cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    cx.update_editor(|editor, cx| {
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
+        assert_eq!(editor.text(cx), "fn foo() {\n  \n}");
+
+        // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
+        editor.tab(&Default::default(), cx);
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.text(cx), "fn foo() {\n    \n}");
+        assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
+
+        // Tabbing again accepts the suggestion.
+        editor.tab(&Default::default(), cx);
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.text(cx), "fn foo() {\n    let x = 4;\n}");
+        assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
+    });
+}
+
+#[gpui::test]
+async fn test_copilot_completion_invalidation(
+    deterministic: Arc<Deterministic>,
+    cx: &mut gpui::TestAppContext,
+) {
+    init_test(cx, |_| {});
+
+    let (copilot, copilot_lsp) = Copilot::fake(cx);
+    cx.update(|cx| cx.set_global(copilot));
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.set_state(indoc! {"
+        one
+        twห‡
+        three
+    "});
+
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "two.foo()".into(),
+            range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+    cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    cx.update_editor(|editor, cx| {
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
+        assert_eq!(editor.text(cx), "one\ntw\nthree\n");
+
+        editor.backspace(&Default::default(), cx);
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
+        assert_eq!(editor.text(cx), "one\nt\nthree\n");
+
+        editor.backspace(&Default::default(), cx);
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
+        assert_eq!(editor.text(cx), "one\n\nthree\n");
+
+        // Deleting across the original suggestion range invalidates it.
+        editor.backspace(&Default::default(), cx);
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one\nthree\n");
+        assert_eq!(editor.text(cx), "one\nthree\n");
+
+        // Undoing the deletion restores the suggestion.
+        editor.undo(&Default::default(), cx);
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
+        assert_eq!(editor.text(cx), "one\n\nthree\n");
+    });
+}
+
+#[gpui::test]
+async fn test_copilot_multibuffer(
+    deterministic: Arc<Deterministic>,
+    cx: &mut gpui::TestAppContext,
+) {
+    init_test(cx, |_| {});
+
+    let (copilot, copilot_lsp) = Copilot::fake(cx);
+    cx.update(|cx| cx.set_global(copilot));
+
+    let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n"));
+    let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n"));
+    let multibuffer = cx.add_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        multibuffer.push_excerpts(
+            buffer_1.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(2, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multibuffer.push_excerpts(
+            buffer_2.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(2, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multibuffer
+    });
+    let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
+
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "b = 2 + a".into(),
+            range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+    editor.update(cx, |editor, cx| {
+        // Ensure copilot suggestions are shown for the first excerpt.
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
+        });
+        editor.next_copilot_suggestion(&Default::default(), cx);
+    });
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    editor.update(cx, |editor, cx| {
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(
+            editor.display_text(cx),
+            "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
+        );
+        assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
+    });
+
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "d = 4 + c".into(),
+            range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+    editor.update(cx, |editor, cx| {
+        // Move to another excerpt, ensuring the suggestion gets cleared.
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
+        });
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(
+            editor.display_text(cx),
+            "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
+        );
+        assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
+
+        // Type a character, ensuring we don't even try to interpolate the previous suggestion.
+        editor.handle_input(" ", cx);
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(
+            editor.display_text(cx),
+            "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
+        );
+        assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
+    });
+
+    // Ensure the new suggestion is displayed when the debounce timeout expires.
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    editor.update(cx, |editor, cx| {
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(
+            editor.display_text(cx),
+            "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
+        );
+        assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
+    });
+}
+
+#[gpui::test]
+async fn test_copilot_disabled_globs(
+    deterministic: Arc<Deterministic>,
+    cx: &mut gpui::TestAppContext,
+) {
+    init_test(cx, |settings| {
+        settings
+            .copilot
+            .get_or_insert(Default::default())
+            .disabled_globs = Some(vec![".env*".to_string()]);
+    });
+
+    let (copilot, copilot_lsp) = Copilot::fake(cx);
+    cx.update(|cx| cx.set_global(copilot));
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/test",
+        json!({
+            ".env": "SECRET=something\n",
+            "README.md": "hello\n"
+        }),
+    )
+    .await;
+    let project = Project::test(fs, ["/test".as_ref()], cx).await;
+
+    let private_buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/test/.env", cx)
+        })
+        .await
+        .unwrap();
+    let public_buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/test/README.md", cx)
+        })
+        .await
+        .unwrap();
+
+    let multibuffer = cx.add_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        multibuffer.push_excerpts(
+            private_buffer.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(1, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multibuffer.push_excerpts(
+            public_buffer.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(1, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multibuffer
+    });
+    let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
+
+    let mut copilot_requests = copilot_lsp
+        .handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| async move {
+            Ok(copilot::request::GetCompletionsResult {
+                completions: vec![copilot::request::Completion {
+                    text: "next line".into(),
+                    range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
+                    ..Default::default()
+                }],
+            })
+        });
+
+    editor.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |selections| {
+            selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
+        });
+        editor.next_copilot_suggestion(&Default::default(), cx);
+    });
+
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    assert!(copilot_requests.try_next().is_err());
+
+    editor.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
+        });
+        editor.next_copilot_suggestion(&Default::default(), cx);
+    });
+
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    assert!(copilot_requests.try_next().is_ok());
+}
+
+#[gpui::test]
+async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            brackets: BracketPairConfig {
+                pairs: vec![BracketPair {
+                    start: "{".to_string(),
+                    end: "}".to_string(),
+                    close: true,
+                    newline: true,
+                }],
+                disabled_scopes_by_bracket_ix: Vec::new(),
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
+                    first_trigger_character: "{".to_string(),
+                    more_trigger_character: None,
+                }),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "main.rs": "fn main() { let a = 5; }",
+            "other.rs": "// Test file",
+        }),
+    )
+    .await;
+    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+    let workspace = cx
+        .add_window(|cx| Workspace::test_new(project.clone(), cx))
+        .root(cx);
+    let worktree_id = workspace.update(cx, |workspace, cx| {
+        workspace.project().read_with(cx, |project, cx| {
+            project.worktrees(cx).next().unwrap().read(cx).id()
+        })
+    });
+
+    let buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/a/main.rs", cx)
+        })
+        .await
+        .unwrap();
+    cx.foreground().run_until_parked();
+    cx.foreground().start_waiting();
+    let fake_server = fake_servers.next().await.unwrap();
+    let editor_handle = workspace
+        .update(cx, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
+        assert_eq!(
+            params.text_document_position.text_document.uri,
+            lsp::Url::from_file_path("/a/main.rs").unwrap(),
+        );
+        assert_eq!(
+            params.text_document_position.position,
+            lsp::Position::new(0, 21),
+        );
+
+        Ok(Some(vec![lsp::TextEdit {
+            new_text: "]".to_string(),
+            range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
+        }]))
+    });
+
+    editor_handle.update(cx, |editor, cx| {
+        cx.focus(&editor_handle);
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
+        });
+        editor.handle_input("{", cx);
+    });
+
+    cx.foreground().run_until_parked();
+
+    buffer.read_with(cx, |buffer, _| {
+        assert_eq!(
+            buffer.text(),
+            "fn main() { let a = {5}; }",
+            "No extra braces from on type formatting should appear in the buffer"
+        )
+    });
+}
+
+#[gpui::test]
+async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language_name: Arc<str> = "Rust".into();
+    let mut language = Language::new(
+        LanguageConfig {
+            name: Arc::clone(&language_name),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+
+    let server_restarts = Arc::new(AtomicUsize::new(0));
+    let closure_restarts = Arc::clone(&server_restarts);
+    let language_server_name = "test language server";
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: language_server_name,
+            initialization_options: Some(json!({
+                "testOptionValue": true
+            })),
+            initializer: Some(Box::new(move |fake_server| {
+                let task_restarts = Arc::clone(&closure_restarts);
+                fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
+                    task_restarts.fetch_add(1, atomic::Ordering::Release);
+                    futures::future::ready(Ok(()))
+                });
+            })),
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "main.rs": "fn main() { let a = 5; }",
+            "other.rs": "// Test file",
+        }),
+    )
+    .await;
+    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+    let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+    let _buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/a/main.rs", cx)
+        })
+        .await
+        .unwrap();
+    let _fake_server = fake_servers.next().await.unwrap();
+    update_test_language_settings(cx, |language_settings| {
+        language_settings.languages.insert(
+            Arc::clone(&language_name),
+            LanguageSettingsContent {
+                tab_size: NonZeroU32::new(8),
+                ..Default::default()
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        0,
+        "Should not restart LSP server on an unrelated change"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            "Some other server name".into(),
+            LspSettings {
+                initialization_options: Some(json!({
+                    "some other init value": false
+                })),
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        0,
+        "Should not restart LSP server on an unrelated LSP settings change"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            language_server_name.into(),
+            LspSettings {
+                initialization_options: Some(json!({
+                    "anotherInitValue": false
+                })),
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        1,
+        "Should restart LSP server on a related LSP settings change"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            language_server_name.into(),
+            LspSettings {
+                initialization_options: Some(json!({
+                    "anotherInitValue": false
+                })),
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        1,
+        "Should not restart LSP server on a related LSP settings change that is the same"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            language_server_name.into(),
+            LspSettings {
+                initialization_options: None,
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        2,
+        "Should restart LSP server on another related LSP settings change"
+    );
+}
+
+#[gpui::test]
+async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string()]),
+                resolve_provider: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.set_state(indoc! {"fn main() { let a = 2ห‡; }"});
+    cx.simulate_keystroke(".");
+    let completion_item = lsp::CompletionItem {
+        label: "some".into(),
+        kind: Some(lsp::CompletionItemKind::SNIPPET),
+        detail: Some("Wrap the expression in an `Option::Some`".to_string()),
+        documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
+            kind: lsp::MarkupKind::Markdown,
+            value: "```rust\nSome(2)\n```".to_string(),
+        })),
+        deprecated: Some(false),
+        sort_text: Some("fffffff2".to_string()),
+        filter_text: Some("some".to_string()),
+        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+            range: lsp::Range {
+                start: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+                end: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+            },
+            new_text: "Some(2)".to_string(),
+        })),
+        additional_text_edits: Some(vec![lsp::TextEdit {
+            range: lsp::Range {
+                start: lsp::Position {
+                    line: 0,
+                    character: 20,
+                },
+                end: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+            },
+            new_text: "".to_string(),
+        }]),
+        ..Default::default()
+    };
+
+    let closure_completion_item = completion_item.clone();
+    let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
+        let task_completion_item = closure_completion_item.clone();
+        async move {
+            Ok(Some(lsp::CompletionResponse::Array(vec![
+                task_completion_item,
+            ])))
+        }
+    });
+
+    request.next().await;
+
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+    let apply_additional_edits = cx.update_editor(|editor, cx| {
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .unwrap()
+    });
+    cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ห‡; }"});
+
+    cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
+        let task_completion_item = completion_item.clone();
+        async move { Ok(task_completion_item) }
+    })
+    .next()
+    .await
+    .unwrap();
+    apply_additional_edits.await.unwrap();
+    cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ห‡; }"});
+}
+
+#[gpui::test]
+async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new(
+        Language::new(
+            LanguageConfig {
+                path_suffixes: vec!["jsx".into()],
+                overrides: [(
+                    "element".into(),
+                    LanguageConfigOverride {
+                        word_characters: Override::Set(['-'].into_iter().collect()),
+                        ..Default::default()
+                    },
+                )]
+                .into_iter()
+                .collect(),
+                ..Default::default()
+            },
+            Some(tree_sitter_typescript::language_tsx()),
+        )
+        .with_override_query("(jsx_self_closing_element) @element")
+        .unwrap(),
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![":".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.lsp
+        .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
+            Ok(Some(lsp::CompletionResponse::Array(vec![
+                lsp::CompletionItem {
+                    label: "bg-blue".into(),
+                    ..Default::default()
+                },
+                lsp::CompletionItem {
+                    label: "bg-red".into(),
+                    ..Default::default()
+                },
+                lsp::CompletionItem {
+                    label: "bg-yellow".into(),
+                    ..Default::default()
+                },
+            ])))
+        });
+
+    cx.set_state(r#"<p class="bgห‡" />"#);
+
+    // Trigger completion when typing a dash, because the dash is an extra
+    // word character in the 'element' scope, which contains the cursor.
+    cx.simulate_keystroke("-");
+    cx.foreground().run_until_parked();
+    cx.update_editor(|editor, _| {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+            assert_eq!(
+                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                &["bg-red", "bg-blue", "bg-yellow"]
+            );
+        } else {
+            panic!("expected completion menu to be open");
+        }
+    });
+
+    cx.simulate_keystroke("l");
+    cx.foreground().run_until_parked();
+    cx.update_editor(|editor, _| {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+            assert_eq!(
+                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                &["bg-blue", "bg-yellow"]
+            );
+        } else {
+            panic!("expected completion menu to be open");
+        }
+    });
+
+    // When filtering completions, consider the character after the '-' to
+    // be the start of a subword.
+    cx.set_state(r#"<p class="yelห‡" />"#);
+    cx.simulate_keystroke("l");
+    cx.foreground().run_until_parked();
+    cx.update_editor(|editor, _| {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+            assert_eq!(
+                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                &["bg-yellow"]
+            );
+        } else {
+            panic!("expected completion menu to be open");
+        }
+    });
+}
+
+#[gpui::test]
+async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
+    });
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            prettier_parser_name: Some("test_parser".to_string()),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+
+    let test_plugin = "test_plugin";
+    let _ = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            prettier_plugins: vec![test_plugin],
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
+    project.update(cx, |project, _| {
+        project.languages().add(Arc::new(language));
+    });
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+        .await
+        .unwrap();
+
+    let buffer_text = "one\ntwo\nthree\n";
+    let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
+
+    let format = editor.update(cx, |editor, cx| {
+        editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+    });
+    format.await.unwrap();
+    assert_eq!(
+        editor.read_with(cx, |editor, cx| editor.text(cx)),
+        buffer_text.to_string() + prettier_format_suffix,
+        "Test prettier formatting was not applied to the original buffer text",
+    );
+
+    update_test_language_settings(cx, |settings| {
+        settings.defaults.formatter = Some(language_settings::Formatter::Auto)
+    });
+    let format = editor.update(cx, |editor, cx| {
+        editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+    });
+    format.await.unwrap();
+    assert_eq!(
+        editor.read_with(cx, |editor, cx| editor.text(cx)),
+        buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
+        "Autoformatting (via test prettier) was not applied to the original buffer text",
+    );
+}
+
+fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
+    let point = DisplayPoint::new(row as u32, column as u32);
+    point..point
+}
+
+fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext<Editor>) {
+    let (text, ranges) = marked_text_ranges(marked_text, true);
+    assert_eq!(view.text(cx), text);
+    assert_eq!(
+        view.selections.ranges(cx),
+        ranges,
+        "Assert selections are {}",
+        marked_text
+    );
+}
+
+/// Handle completion request passing a marked string specifying where the completion
+/// should be triggered from using '|' character, what range should be replaced, and what completions
+/// should be returned using '<' and '>' to delimit the range
+pub fn handle_completion_request<'a>(
+    cx: &mut EditorLspTestContext<'a>,
+    marked_string: &str,
+    completions: Vec<&'static str>,
+) -> impl Future<Output = ()> {
+    let complete_from_marker: TextRangeMarker = '|'.into();
+    let replace_range_marker: TextRangeMarker = ('<', '>').into();
+    let (_, mut marked_ranges) = marked_text_ranges_by(
+        marked_string,
+        vec![complete_from_marker.clone(), replace_range_marker.clone()],
+    );
+
+    let complete_from_position =
+        cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
+    let replace_range =
+        cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
+
+    let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
+        let completions = completions.clone();
+        async move {
+            assert_eq!(params.text_document_position.text_document.uri, url.clone());
+            assert_eq!(
+                params.text_document_position.position,
+                complete_from_position
+            );
+            Ok(Some(lsp::CompletionResponse::Array(
+                completions
+                    .iter()
+                    .map(|completion_text| lsp::CompletionItem {
+                        label: completion_text.to_string(),
+                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                            range: replace_range,
+                            new_text: completion_text.to_string(),
+                        })),
+                        ..Default::default()
+                    })
+                    .collect(),
+            )))
+        }
+    });
+
+    async move {
+        request.next().await;
+    }
+}
+
+fn handle_resolve_completion_request<'a>(
+    cx: &mut EditorLspTestContext<'a>,
+    edits: Option<Vec<(&'static str, &'static str)>>,
+) -> impl Future<Output = ()> {
+    let edits = edits.map(|edits| {
+        edits
+            .iter()
+            .map(|(marked_string, new_text)| {
+                let (_, marked_ranges) = marked_text_ranges(marked_string, false);
+                let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
+                lsp::TextEdit::new(replace_range, new_text.to_string())
+            })
+            .collect::<Vec<_>>()
+    });
+
+    let mut request =
+        cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
+            let edits = edits.clone();
+            async move {
+                Ok(lsp::CompletionItem {
+                    additional_text_edits: edits,
+                    ..Default::default()
+                })
+            }
+        });
+
+    async move {
+        request.next().await;
+    }
+}
+
+fn handle_copilot_completion_request(
+    lsp: &lsp::FakeLanguageServer,
+    completions: Vec<copilot::request::Completion>,
+    completions_cycling: Vec<copilot::request::Completion>,
+) {
+    lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
+        let completions = completions.clone();
+        async move {
+            Ok(copilot::request::GetCompletionsResult {
+                completions: completions.clone(),
+            })
+        }
+    });
+    lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
+        let completions_cycling = completions_cycling.clone();
+        async move {
+            Ok(copilot::request::GetCompletionsResult {
+                completions: completions_cycling.clone(),
+            })
+        }
+    });
+}
+
+pub(crate) fn update_test_language_settings(
+    cx: &mut TestAppContext,
+    f: impl Fn(&mut AllLanguageSettingsContent),
+) {
+    cx.update(|cx| {
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, f);
+        });
+    });
+}
+
+pub(crate) fn update_test_project_settings(
+    cx: &mut TestAppContext,
+    f: impl Fn(&mut ProjectSettings),
+) {
+    cx.update(|cx| {
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            store.update_user_settings::<ProjectSettings>(cx, f);
+        });
+    });
+}
+
+pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
+    cx.foreground().forbid_parking();
+
+    cx.update(|cx| {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+        client::init_settings(cx);
+        language::init(cx);
+        Project::init_settings(cx);
+        workspace::init_settings(cx);
+        crate::init(cx);
+    });
+
+    update_test_language_settings(cx, f);
+}

crates/editor2/src/element.rs ๐Ÿ”—

@@ -0,0 +1,3478 @@
+use super::{
+    display_map::{BlockContext, ToDisplayPoint},
+    Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, SelectPhase, SoftWrap, ToPoint,
+    MAX_LINE_LEN,
+};
+use crate::{
+    display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock},
+    editor_settings::ShowScrollbar,
+    git::{diff_hunk_to_display, DisplayDiffHunk},
+    hover_popover::{
+        hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
+        MIN_POPOVER_LINE_HEIGHT,
+    },
+    link_go_to_definition::{
+        go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
+        update_inlay_link_and_hover_points, GoToDefinitionTrigger,
+    },
+    mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
+};
+use collections::{BTreeMap, HashMap};
+use git::diff::DiffHunkStatus;
+use gpui::{
+    color::Color,
+    elements::*,
+    fonts::TextStyle,
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+        PathBuilder,
+    },
+    json::{self, ToJson},
+    platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent},
+    text_layout::{self, Line, RunStyle, TextLayoutCache},
+    AnyElement, Axis, CursorRegion, Element, EventContext, FontCache, MouseRegion, Quad,
+    SizeConstraint, ViewContext, WindowContext,
+};
+use itertools::Itertools;
+use json::json;
+use language::{
+    language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection,
+};
+use project::{
+    project_settings::{GitGutterSetting, ProjectSettings},
+    ProjectPath,
+};
+use smallvec::SmallVec;
+use std::{
+    borrow::Cow,
+    cmp::{self, Ordering},
+    fmt::Write,
+    iter,
+    ops::Range,
+    sync::Arc,
+};
+use text::Point;
+use theme::SelectionStyle;
+use workspace::item::Item;
+
+enum FoldMarkers {}
+
+struct SelectionLayout {
+    head: DisplayPoint,
+    cursor_shape: CursorShape,
+    is_newest: bool,
+    is_local: bool,
+    range: Range<DisplayPoint>,
+    active_rows: Range<u32>,
+}
+
+impl SelectionLayout {
+    fn new<T: ToPoint + ToDisplayPoint + Clone>(
+        selection: Selection<T>,
+        line_mode: bool,
+        cursor_shape: CursorShape,
+        map: &DisplaySnapshot,
+        is_newest: bool,
+        is_local: bool,
+    ) -> Self {
+        let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
+        let display_selection = point_selection.map(|p| p.to_display_point(map));
+        let mut range = display_selection.range();
+        let mut head = display_selection.head();
+        let mut active_rows = map.prev_line_boundary(point_selection.start).1.row()
+            ..map.next_line_boundary(point_selection.end).1.row();
+
+        // vim visual line mode
+        if line_mode {
+            let point_range = map.expand_to_line(point_selection.range());
+            range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map);
+        }
+
+        // any vim visual mode (including line mode)
+        if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed {
+            if head.column() > 0 {
+                head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left)
+            } else if head.row() > 0 && head != map.max_point() {
+                head = map.clip_point(
+                    DisplayPoint::new(head.row() - 1, map.line_len(head.row() - 1)),
+                    Bias::Left,
+                );
+                // updating range.end is a no-op unless you're cursor is
+                // on the newline containing a multi-buffer divider
+                // in which case the clip_point may have moved the head up
+                // an additional row.
+                range.end = DisplayPoint::new(head.row() + 1, 0);
+                active_rows.end = head.row();
+            }
+        }
+
+        Self {
+            head,
+            cursor_shape,
+            is_newest,
+            is_local,
+            range,
+            active_rows,
+        }
+    }
+}
+
+pub struct EditorElement {
+    style: Arc<EditorStyle>,
+}
+
+impl EditorElement {
+    pub fn new(style: EditorStyle) -> Self {
+        Self {
+            style: Arc::new(style),
+        }
+    }
+
+    fn attach_mouse_handlers(
+        position_map: &Arc<PositionMap>,
+        has_popovers: bool,
+        visible_bounds: RectF,
+        text_bounds: RectF,
+        gutter_bounds: RectF,
+        bounds: RectF,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        enum EditorElementMouseHandlers {}
+        let view_id = cx.view_id();
+        cx.scene().push_mouse_region(
+            MouseRegion::new::<EditorElementMouseHandlers>(view_id, view_id, visible_bounds)
+                .on_down(MouseButton::Left, {
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if !Self::mouse_down(
+                            editor,
+                            event.platform_event,
+                            position_map.as_ref(),
+                            text_bounds,
+                            gutter_bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event();
+                        }
+                    }
+                })
+                .on_down(MouseButton::Right, {
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if !Self::mouse_right_down(
+                            editor,
+                            event.position,
+                            position_map.as_ref(),
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event();
+                        }
+                    }
+                })
+                .on_up(MouseButton::Left, {
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if !Self::mouse_up(
+                            editor,
+                            event.position,
+                            event.cmd,
+                            event.shift,
+                            event.alt,
+                            position_map.as_ref(),
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event()
+                        }
+                    }
+                })
+                .on_drag(MouseButton::Left, {
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if event.end {
+                            return;
+                        }
+
+                        if !Self::mouse_dragged(
+                            editor,
+                            event.platform_event,
+                            position_map.as_ref(),
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event()
+                        }
+                    }
+                })
+                .on_move({
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if !Self::mouse_moved(
+                            editor,
+                            event.platform_event,
+                            &position_map,
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event()
+                        }
+                    }
+                })
+                .on_move_out(move |_, editor: &mut Editor, cx| {
+                    if has_popovers {
+                        hide_hover(editor, cx);
+                    }
+                })
+                .on_scroll({
+                    let position_map = position_map.clone();
+                    move |event, editor, cx| {
+                        if !Self::scroll(
+                            editor,
+                            event.position,
+                            *event.delta.raw(),
+                            event.delta.precise(),
+                            &position_map,
+                            bounds,
+                            cx,
+                        ) {
+                            cx.propagate_event()
+                        }
+                    }
+                }),
+        );
+
+        enum GutterHandlers {}
+        let view_id = cx.view_id();
+        let region_id = cx.view_id() + 1;
+        cx.scene().push_mouse_region(
+            MouseRegion::new::<GutterHandlers>(view_id, region_id, gutter_bounds).on_hover(
+                |hover, editor: &mut Editor, cx| {
+                    editor.gutter_hover(
+                        &GutterHover {
+                            hovered: hover.started,
+                        },
+                        cx,
+                    );
+                },
+            ),
+        )
+    }
+
+    fn mouse_down(
+        editor: &mut Editor,
+        MouseButtonEvent {
+            position,
+            modifiers:
+                Modifiers {
+                    shift,
+                    ctrl,
+                    alt,
+                    cmd,
+                    ..
+                },
+            mut click_count,
+            ..
+        }: MouseButtonEvent,
+        position_map: &PositionMap,
+        text_bounds: RectF,
+        gutter_bounds: RectF,
+        cx: &mut EventContext<Editor>,
+    ) -> bool {
+        if gutter_bounds.contains_point(position) {
+            click_count = 3; // Simulate triple-click when clicking the gutter to select lines
+        } else if !text_bounds.contains_point(position) {
+            return false;
+        }
+
+        let point_for_position = position_map.point_for_position(text_bounds, position);
+        let position = point_for_position.previous_valid;
+        if shift && alt {
+            editor.select(
+                SelectPhase::BeginColumnar {
+                    position,
+                    goal_column: point_for_position.exact_unclipped.column(),
+                },
+                cx,
+            );
+        } else if shift && !ctrl && !alt && !cmd {
+            editor.select(
+                SelectPhase::Extend {
+                    position,
+                    click_count,
+                },
+                cx,
+            );
+        } else {
+            editor.select(
+                SelectPhase::Begin {
+                    position,
+                    add: alt,
+                    click_count,
+                },
+                cx,
+            );
+        }
+
+        true
+    }
+
+    fn mouse_right_down(
+        editor: &mut Editor,
+        position: Vector2F,
+        position_map: &PositionMap,
+        text_bounds: RectF,
+        cx: &mut EventContext<Editor>,
+    ) -> bool {
+        if !text_bounds.contains_point(position) {
+            return false;
+        }
+        let point_for_position = position_map.point_for_position(text_bounds, position);
+        mouse_context_menu::deploy_context_menu(
+            editor,
+            position,
+            point_for_position.previous_valid,
+            cx,
+        );
+        true
+    }
+
+    fn mouse_up(
+        editor: &mut Editor,
+        position: Vector2F,
+        cmd: bool,
+        shift: bool,
+        alt: bool,
+        position_map: &PositionMap,
+        text_bounds: RectF,
+        cx: &mut EventContext<Editor>,
+    ) -> bool {
+        let end_selection = editor.has_pending_selection();
+        let pending_nonempty_selections = editor.has_pending_nonempty_selection();
+
+        if end_selection {
+            editor.select(SelectPhase::End, cx);
+        }
+
+        if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
+            let point = position_map.point_for_position(text_bounds, position);
+            let could_be_inlay = point.as_valid().is_none();
+            if shift || could_be_inlay {
+                go_to_fetched_type_definition(editor, point, alt, cx);
+            } else {
+                go_to_fetched_definition(editor, point, alt, cx);
+            }
+
+            return true;
+        }
+
+        end_selection
+    }
+
+    fn mouse_dragged(
+        editor: &mut Editor,
+        MouseMovedEvent {
+            modifiers: Modifiers { cmd, shift, .. },
+            position,
+            ..
+        }: MouseMovedEvent,
+        position_map: &PositionMap,
+        text_bounds: RectF,
+        cx: &mut EventContext<Editor>,
+    ) -> bool {
+        // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
+        // Don't trigger hover popover if mouse is hovering over context menu
+        let point = if text_bounds.contains_point(position) {
+            position_map
+                .point_for_position(text_bounds, position)
+                .as_valid()
+        } else {
+            None
+        };
+
+        update_go_to_definition_link(
+            editor,
+            point.map(GoToDefinitionTrigger::Text),
+            cmd,
+            shift,
+            cx,
+        );
+
+        if editor.has_pending_selection() {
+            let mut scroll_delta = Vector2F::zero();
+
+            let vertical_margin = position_map.line_height.min(text_bounds.height() / 3.0);
+            let top = text_bounds.origin_y() + vertical_margin;
+            let bottom = text_bounds.lower_left().y() - vertical_margin;
+            if position.y() < top {
+                scroll_delta.set_y(-scale_vertical_mouse_autoscroll_delta(top - position.y()))
+            }
+            if position.y() > bottom {
+                scroll_delta.set_y(scale_vertical_mouse_autoscroll_delta(position.y() - bottom))
+            }
+
+            let horizontal_margin = position_map.line_height.min(text_bounds.width() / 3.0);
+            let left = text_bounds.origin_x() + horizontal_margin;
+            let right = text_bounds.upper_right().x() - horizontal_margin;
+            if position.x() < left {
+                scroll_delta.set_x(-scale_horizontal_mouse_autoscroll_delta(
+                    left - position.x(),
+                ))
+            }
+            if position.x() > right {
+                scroll_delta.set_x(scale_horizontal_mouse_autoscroll_delta(
+                    position.x() - right,
+                ))
+            }
+
+            let point_for_position = position_map.point_for_position(text_bounds, position);
+
+            editor.select(
+                SelectPhase::Update {
+                    position: point_for_position.previous_valid,
+                    goal_column: point_for_position.exact_unclipped.column(),
+                    scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
+                        .clamp(Vector2F::zero(), position_map.scroll_max),
+                },
+                cx,
+            );
+            hover_at(editor, point, cx);
+            true
+        } else {
+            hover_at(editor, point, cx);
+            false
+        }
+    }
+
+    fn mouse_moved(
+        editor: &mut Editor,
+        MouseMovedEvent {
+            modifiers: Modifiers { shift, cmd, .. },
+            position,
+            ..
+        }: MouseMovedEvent,
+        position_map: &PositionMap,
+        text_bounds: RectF,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
+        // Don't trigger hover popover if mouse is hovering over context menu
+        if text_bounds.contains_point(position) {
+            let point_for_position = position_map.point_for_position(text_bounds, position);
+            match point_for_position.as_valid() {
+                Some(point) => {
+                    update_go_to_definition_link(
+                        editor,
+                        Some(GoToDefinitionTrigger::Text(point)),
+                        cmd,
+                        shift,
+                        cx,
+                    );
+                    hover_at(editor, Some(point), cx);
+                }
+                None => {
+                    update_inlay_link_and_hover_points(
+                        &position_map.snapshot,
+                        point_for_position,
+                        editor,
+                        cmd,
+                        shift,
+                        cx,
+                    );
+                }
+            }
+        } else {
+            update_go_to_definition_link(editor, None, cmd, shift, cx);
+            hover_at(editor, None, cx);
+        }
+
+        true
+    }
+
+    fn scroll(
+        editor: &mut Editor,
+        position: Vector2F,
+        mut delta: Vector2F,
+        precise: bool,
+        position_map: &PositionMap,
+        bounds: RectF,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        if !bounds.contains_point(position) {
+            return false;
+        }
+
+        let line_height = position_map.line_height;
+        let max_glyph_width = position_map.em_width;
+
+        let axis = if precise {
+            //Trackpad
+            position_map.snapshot.ongoing_scroll.filter(&mut delta)
+        } else {
+            //Not trackpad
+            delta *= vec2f(max_glyph_width, line_height);
+            None //Resets ongoing scroll
+        };
+
+        let scroll_position = position_map.snapshot.scroll_position();
+        let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width;
+        let y = (scroll_position.y() * line_height - delta.y()) / line_height;
+        let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max);
+        editor.scroll(scroll_position, axis, cx);
+
+        true
+    }
+
+    fn paint_background(
+        &self,
+        gutter_bounds: RectF,
+        text_bounds: RectF,
+        layout: &LayoutState,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let bounds = gutter_bounds.union_rect(text_bounds);
+        let scroll_top =
+            layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
+        cx.scene().push_quad(Quad {
+            bounds: gutter_bounds,
+            background: Some(self.style.gutter_background),
+            border: Border::new(0., Color::transparent_black()).into(),
+            corner_radii: Default::default(),
+        });
+        cx.scene().push_quad(Quad {
+            bounds: text_bounds,
+            background: Some(self.style.background),
+            border: Border::new(0., Color::transparent_black()).into(),
+            corner_radii: Default::default(),
+        });
+
+        if let EditorMode::Full = layout.mode {
+            let mut active_rows = layout.active_rows.iter().peekable();
+            while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
+                let mut end_row = *start_row;
+                while active_rows.peek().map_or(false, |r| {
+                    *r.0 == end_row + 1 && r.1 == contains_non_empty_selection
+                }) {
+                    active_rows.next().unwrap();
+                    end_row += 1;
+                }
+
+                if !contains_non_empty_selection {
+                    let origin = vec2f(
+                        bounds.origin_x(),
+                        bounds.origin_y() + (layout.position_map.line_height * *start_row as f32)
+                            - scroll_top,
+                    );
+                    let size = vec2f(
+                        bounds.width(),
+                        layout.position_map.line_height * (end_row - start_row + 1) as f32,
+                    );
+                    cx.scene().push_quad(Quad {
+                        bounds: RectF::new(origin, size),
+                        background: Some(self.style.active_line_background),
+                        border: Border::default().into(),
+                        corner_radii: Default::default(),
+                    });
+                }
+            }
+
+            if let Some(highlighted_rows) = &layout.highlighted_rows {
+                let origin = vec2f(
+                    bounds.origin_x(),
+                    bounds.origin_y()
+                        + (layout.position_map.line_height * highlighted_rows.start as f32)
+                        - scroll_top,
+                );
+                let size = vec2f(
+                    bounds.width(),
+                    layout.position_map.line_height * highlighted_rows.len() as f32,
+                );
+                cx.scene().push_quad(Quad {
+                    bounds: RectF::new(origin, size),
+                    background: Some(self.style.highlighted_line_background),
+                    border: Border::default().into(),
+                    corner_radii: Default::default(),
+                });
+            }
+
+            let scroll_left =
+                layout.position_map.snapshot.scroll_position().x() * layout.position_map.em_width;
+
+            for (wrap_position, active) in layout.wrap_guides.iter() {
+                let x =
+                    (text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.)
+                        - scroll_left;
+
+                if x < text_bounds.origin_x()
+                    || (layout.show_scrollbars && x > self.scrollbar_left(&bounds))
+                {
+                    continue;
+                }
+
+                let color = if *active {
+                    self.style.active_wrap_guide
+                } else {
+                    self.style.wrap_guide
+                };
+                cx.scene().push_quad(Quad {
+                    bounds: RectF::new(
+                        vec2f(x, text_bounds.origin_y()),
+                        vec2f(1., text_bounds.height()),
+                    ),
+                    background: Some(color),
+                    border: Border::new(0., Color::transparent_black()).into(),
+                    corner_radii: Default::default(),
+                });
+            }
+        }
+    }
+
+    fn paint_gutter(
+        &mut self,
+        bounds: RectF,
+        visible_bounds: RectF,
+        layout: &mut LayoutState,
+        editor: &mut Editor,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let line_height = layout.position_map.line_height;
+
+        let scroll_position = layout.position_map.snapshot.scroll_position();
+        let scroll_top = scroll_position.y() * line_height;
+
+        let show_gutter = matches!(
+            settings::get::<ProjectSettings>(cx).git.git_gutter,
+            Some(GitGutterSetting::TrackedFiles)
+        );
+
+        if show_gutter {
+            Self::paint_diff_hunks(bounds, layout, cx);
+        }
+
+        for (ix, line) in layout.line_number_layouts.iter().enumerate() {
+            if let Some(line) = line {
+                let line_origin = bounds.origin()
+                    + vec2f(
+                        bounds.width() - line.width() - layout.gutter_padding,
+                        ix as f32 * line_height - (scroll_top % line_height),
+                    );
+
+                line.paint(line_origin, visible_bounds, line_height, cx);
+            }
+        }
+
+        for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() {
+            if let Some(indicator) = fold_indicator.as_mut() {
+                let position = vec2f(
+                    bounds.width() - layout.gutter_padding,
+                    ix as f32 * line_height - (scroll_top % line_height),
+                );
+                let centering_offset = vec2f(
+                    (layout.gutter_padding + layout.gutter_margin - indicator.size().x()) / 2.,
+                    (line_height - indicator.size().y()) / 2.,
+                );
+
+                let indicator_origin = bounds.origin() + position + centering_offset;
+
+                indicator.paint(indicator_origin, visible_bounds, editor, cx);
+            }
+        }
+
+        if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
+            let mut x = 0.;
+            let mut y = *row as f32 * line_height - scroll_top;
+            x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
+            y += (line_height - indicator.size().y()) / 2.;
+            indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, editor, cx);
+        }
+    }
+
+    fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut ViewContext<Editor>) {
+        let diff_style = &theme::current(cx).editor.diff.clone();
+        let line_height = layout.position_map.line_height;
+
+        let scroll_position = layout.position_map.snapshot.scroll_position();
+        let scroll_top = scroll_position.y() * line_height;
+
+        for hunk in &layout.display_hunks {
+            let (display_row_range, status) = match hunk {
+                //TODO: This rendering is entirely a horrible hack
+                &DisplayDiffHunk::Folded { display_row: row } => {
+                    let start_y = row as f32 * line_height - scroll_top;
+                    let end_y = start_y + line_height;
+
+                    let width = diff_style.removed_width_em * line_height;
+                    let highlight_origin = bounds.origin() + vec2f(-width, start_y);
+                    let highlight_size = vec2f(width * 2., end_y - start_y);
+                    let highlight_bounds = RectF::new(highlight_origin, highlight_size);
+
+                    cx.scene().push_quad(Quad {
+                        bounds: highlight_bounds,
+                        background: Some(diff_style.modified),
+                        border: Border::new(0., Color::transparent_black()).into(),
+                        corner_radii: (1. * line_height).into(),
+                    });
+
+                    continue;
+                }
+
+                DisplayDiffHunk::Unfolded {
+                    display_row_range,
+                    status,
+                } => (display_row_range, status),
+            };
+
+            let color = match status {
+                DiffHunkStatus::Added => diff_style.inserted,
+                DiffHunkStatus::Modified => diff_style.modified,
+
+                //TODO: This rendering is entirely a horrible hack
+                DiffHunkStatus::Removed => {
+                    let row = display_row_range.start;
+
+                    let offset = line_height / 2.;
+                    let start_y = row as f32 * line_height - offset - scroll_top;
+                    let end_y = start_y + line_height;
+
+                    let width = diff_style.removed_width_em * line_height;
+                    let highlight_origin = bounds.origin() + vec2f(-width, start_y);
+                    let highlight_size = vec2f(width * 2., end_y - start_y);
+                    let highlight_bounds = RectF::new(highlight_origin, highlight_size);
+
+                    cx.scene().push_quad(Quad {
+                        bounds: highlight_bounds,
+                        background: Some(diff_style.deleted),
+                        border: Border::new(0., Color::transparent_black()).into(),
+                        corner_radii: (1. * line_height).into(),
+                    });
+
+                    continue;
+                }
+            };
+
+            let start_row = display_row_range.start;
+            let end_row = display_row_range.end;
+
+            let start_y = start_row as f32 * line_height - scroll_top;
+            let end_y = end_row as f32 * line_height - scroll_top;
+
+            let width = diff_style.width_em * line_height;
+            let highlight_origin = bounds.origin() + vec2f(-width, start_y);
+            let highlight_size = vec2f(width * 2., end_y - start_y);
+            let highlight_bounds = RectF::new(highlight_origin, highlight_size);
+
+            cx.scene().push_quad(Quad {
+                bounds: highlight_bounds,
+                background: Some(color),
+                border: Border::new(0., Color::transparent_black()).into(),
+                corner_radii: (diff_style.corner_radius * line_height).into(),
+            });
+        }
+    }
+
+    fn paint_text(
+        &mut self,
+        bounds: RectF,
+        visible_bounds: RectF,
+        layout: &mut LayoutState,
+        editor: &mut Editor,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let style = &self.style;
+        let scroll_position = layout.position_map.snapshot.scroll_position();
+        let start_row = layout.visible_display_row_range.start;
+        let scroll_top = scroll_position.y() * layout.position_map.line_height;
+        let max_glyph_width = layout.position_map.em_width;
+        let scroll_left = scroll_position.x() * max_glyph_width;
+        let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
+        let line_end_overshoot = 0.15 * layout.position_map.line_height;
+        let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
+
+        cx.scene().push_layer(Some(bounds));
+
+        cx.scene().push_cursor_region(CursorRegion {
+            bounds,
+            style: if !editor.link_go_to_definition_state.definitions.is_empty() {
+                CursorStyle::PointingHand
+            } else {
+                CursorStyle::IBeam
+            },
+        });
+
+        let fold_corner_radius =
+            self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height;
+        for (id, range, color) in layout.fold_ranges.iter() {
+            self.paint_highlighted_range(
+                range.clone(),
+                *color,
+                fold_corner_radius,
+                fold_corner_radius * 2.,
+                layout,
+                content_origin,
+                scroll_top,
+                scroll_left,
+                bounds,
+                cx,
+            );
+
+            for bound in range_to_bounds(
+                &range,
+                content_origin,
+                scroll_left,
+                scroll_top,
+                &layout.visible_display_row_range,
+                line_end_overshoot,
+                &layout.position_map,
+            ) {
+                cx.scene().push_cursor_region(CursorRegion {
+                    bounds: bound,
+                    style: CursorStyle::PointingHand,
+                });
+
+                let display_row = range.start.row();
+
+                let buffer_row = DisplayPoint::new(display_row, 0)
+                    .to_point(&layout.position_map.snapshot.display_snapshot)
+                    .row;
+
+                let view_id = cx.view_id();
+                cx.scene().push_mouse_region(
+                    MouseRegion::new::<FoldMarkers>(view_id, *id as usize, bound)
+                        .on_click(MouseButton::Left, move |_, editor: &mut Editor, cx| {
+                            editor.unfold_at(&UnfoldAt { buffer_row }, cx)
+                        })
+                        .with_notify_on_hover(true)
+                        .with_notify_on_click(true),
+                )
+            }
+        }
+
+        for (range, color) in &layout.highlighted_ranges {
+            self.paint_highlighted_range(
+                range.clone(),
+                *color,
+                0.,
+                line_end_overshoot,
+                layout,
+                content_origin,
+                scroll_top,
+                scroll_left,
+                bounds,
+                cx,
+            );
+        }
+
+        let mut cursors = SmallVec::<[Cursor; 32]>::new();
+        let corner_radius = 0.15 * layout.position_map.line_height;
+        let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
+
+        for (selection_style, selections) in &layout.selections {
+            for selection in selections {
+                self.paint_highlighted_range(
+                    selection.range.clone(),
+                    selection_style.selection,
+                    corner_radius,
+                    corner_radius * 2.,
+                    layout,
+                    content_origin,
+                    scroll_top,
+                    scroll_left,
+                    bounds,
+                    cx,
+                );
+
+                if selection.is_local && !selection.range.is_empty() {
+                    invisible_display_ranges.push(selection.range.clone());
+                }
+                if !selection.is_local || editor.show_local_cursors(cx) {
+                    let cursor_position = selection.head;
+                    if layout
+                        .visible_display_row_range
+                        .contains(&cursor_position.row())
+                    {
+                        let cursor_row_layout = &layout.position_map.line_layouts
+                            [(cursor_position.row() - start_row) as usize]
+                            .line;
+                        let cursor_column = cursor_position.column() as usize;
+
+                        let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
+                        let mut block_width =
+                            cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
+                        if block_width == 0.0 {
+                            block_width = layout.position_map.em_width;
+                        }
+                        let block_text = if let CursorShape::Block = selection.cursor_shape {
+                            layout
+                                .position_map
+                                .snapshot
+                                .chars_at(cursor_position)
+                                .next()
+                                .and_then(|(character, _)| {
+                                    let font_id =
+                                        cursor_row_layout.font_for_index(cursor_column)?;
+                                    let text = character.to_string();
+
+                                    Some(cx.text_layout_cache().layout_str(
+                                        &text,
+                                        cursor_row_layout.font_size(),
+                                        &[(
+                                            text.chars().count(),
+                                            RunStyle {
+                                                font_id,
+                                                color: style.background,
+                                                underline: Default::default(),
+                                            },
+                                        )],
+                                    ))
+                                })
+                        } else {
+                            None
+                        };
+
+                        let x = cursor_character_x - scroll_left;
+                        let y = cursor_position.row() as f32 * layout.position_map.line_height
+                            - scroll_top;
+                        if selection.is_newest {
+                            editor.pixel_position_of_newest_cursor = Some(vec2f(
+                                bounds.origin_x() + x + block_width / 2.,
+                                bounds.origin_y() + y + layout.position_map.line_height / 2.,
+                            ));
+                        }
+                        cursors.push(Cursor {
+                            color: selection_style.cursor,
+                            block_width,
+                            origin: vec2f(x, y),
+                            line_height: layout.position_map.line_height,
+                            shape: selection.cursor_shape,
+                            block_text,
+                        });
+                    }
+                }
+            }
+        }
+
+        if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) {
+            for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
+                let row = start_row + ix as u32;
+                line_with_invisibles.draw(
+                    layout,
+                    row,
+                    scroll_top,
+                    content_origin,
+                    scroll_left,
+                    visible_text_bounds,
+                    whitespace_setting,
+                    &invisible_display_ranges,
+                    visible_bounds,
+                    cx,
+                )
+            }
+        }
+
+        cx.scene().push_layer(Some(bounds));
+        for cursor in cursors {
+            cursor.paint(content_origin, cx);
+        }
+        cx.scene().pop_layer();
+
+        if let Some((position, context_menu)) = layout.context_menu.as_mut() {
+            cx.scene().push_stacking_context(None, None);
+            let cursor_row_layout =
+                &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
+            let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
+            let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
+            let mut list_origin = content_origin + vec2f(x, y);
+            let list_width = context_menu.size().x();
+            let list_height = context_menu.size().y();
+
+            // Snap the right edge of the list to the right edge of the window if
+            // its horizontal bounds overflow.
+            if list_origin.x() + list_width > cx.window_size().x() {
+                list_origin.set_x((cx.window_size().x() - list_width).max(0.));
+            }
+
+            if list_origin.y() + list_height > bounds.max_y() {
+                list_origin.set_y(list_origin.y() - layout.position_map.line_height - list_height);
+            }
+
+            context_menu.paint(
+                list_origin,
+                RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
+                editor,
+                cx,
+            );
+
+            cx.scene().pop_stacking_context();
+        }
+
+        if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
+            cx.scene().push_stacking_context(None, None);
+
+            // This is safe because we check on layout whether the required row is available
+            let hovered_row_layout =
+                &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
+
+            // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
+            // height. This is the size we will use to decide whether to render popovers above or below
+            // the hovered line.
+            let first_size = hover_popovers[0].size();
+            let height_to_reserve = first_size.y()
+                + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height;
+
+            // Compute Hovered Point
+            let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
+            let y = position.row() as f32 * layout.position_map.line_height - scroll_top;
+            let hovered_point = content_origin + vec2f(x, y);
+
+            if hovered_point.y() - height_to_reserve > 0.0 {
+                // There is enough space above. Render popovers above the hovered point
+                let mut current_y = hovered_point.y();
+                for hover_popover in hover_popovers {
+                    let size = hover_popover.size();
+                    let mut popover_origin = vec2f(hovered_point.x(), current_y - size.y());
+
+                    let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
+                    if x_out_of_bounds < 0.0 {
+                        popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
+                    }
+
+                    hover_popover.paint(
+                        popover_origin,
+                        RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
+                        editor,
+                        cx,
+                    );
+
+                    current_y = popover_origin.y() - HOVER_POPOVER_GAP;
+                }
+            } else {
+                // There is not enough space above. Render popovers below the hovered point
+                let mut current_y = hovered_point.y() + layout.position_map.line_height;
+                for hover_popover in hover_popovers {
+                    let size = hover_popover.size();
+                    let mut popover_origin = vec2f(hovered_point.x(), current_y);
+
+                    let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
+                    if x_out_of_bounds < 0.0 {
+                        popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
+                    }
+
+                    hover_popover.paint(
+                        popover_origin,
+                        RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
+                        editor,
+                        cx,
+                    );
+
+                    current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP;
+                }
+            }
+
+            cx.scene().pop_stacking_context();
+        }
+
+        cx.scene().pop_layer();
+    }
+
+    fn scrollbar_left(&self, bounds: &RectF) -> f32 {
+        bounds.max_x() - self.style.theme.scrollbar.width
+    }
+
+    fn paint_scrollbar(
+        &mut self,
+        bounds: RectF,
+        layout: &mut LayoutState,
+        editor: &Editor,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        enum ScrollbarMouseHandlers {}
+        if layout.mode != EditorMode::Full {
+            return;
+        }
+
+        let style = &self.style.theme.scrollbar;
+
+        let top = bounds.min_y();
+        let bottom = bounds.max_y();
+        let right = bounds.max_x();
+        let left = self.scrollbar_left(&bounds);
+        let row_range = &layout.scrollbar_row_range;
+        let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
+
+        let mut height = bounds.height();
+        let mut first_row_y_offset = 0.0;
+
+        // Impose a minimum height on the scrollbar thumb
+        let row_height = height / max_row;
+        let min_thumb_height =
+            style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
+        let thumb_height = (row_range.end - row_range.start) * row_height;
+        if thumb_height < min_thumb_height {
+            first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
+            height -= min_thumb_height - thumb_height;
+        }
+
+        let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * row_height };
+
+        let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
+        let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
+        let track_bounds = RectF::from_points(vec2f(left, top), vec2f(right, bottom));
+        let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom));
+
+        if layout.show_scrollbars {
+            cx.scene().push_quad(Quad {
+                bounds: track_bounds,
+                border: style.track.border.into(),
+                background: style.track.background_color,
+                ..Default::default()
+            });
+            let scrollbar_settings = settings::get::<EditorSettings>(cx).scrollbar;
+            let theme = theme::current(cx);
+            let scrollbar_theme = &theme.editor.scrollbar;
+            if layout.is_singleton && scrollbar_settings.selections {
+                let start_anchor = Anchor::min();
+                let end_anchor = Anchor::max();
+                let color = scrollbar_theme.selections;
+                let border = Border {
+                    width: 1.,
+                    color: style.thumb.border.color,
+                    overlay: false,
+                    top: false,
+                    right: true,
+                    bottom: false,
+                    left: true,
+                };
+                let mut push_region = |start: DisplayPoint, end: DisplayPoint| {
+                    let start_y = y_for_row(start.row() as f32);
+                    let mut end_y = y_for_row(end.row() as f32);
+                    if end_y - start_y < 1. {
+                        end_y = start_y + 1.;
+                    }
+                    let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
+
+                    cx.scene().push_quad(Quad {
+                        bounds,
+                        background: Some(color),
+                        border: border.into(),
+                        corner_radii: style.thumb.corner_radii.into(),
+                    })
+                };
+                let background_ranges = editor
+                    .background_highlight_row_ranges::<crate::items::BufferSearchHighlights>(
+                        start_anchor..end_anchor,
+                        &layout.position_map.snapshot,
+                        50000,
+                    );
+                for row in background_ranges {
+                    let start = row.start();
+                    let end = row.end();
+                    push_region(*start, *end);
+                }
+            }
+
+            if layout.is_singleton && scrollbar_settings.git_diff {
+                let diff_style = scrollbar_theme.git.clone();
+                for hunk in layout
+                    .position_map
+                    .snapshot
+                    .buffer_snapshot
+                    .git_diff_hunks_in_range(0..(max_row.floor() as u32))
+                {
+                    let start_display = Point::new(hunk.buffer_range.start, 0)
+                        .to_display_point(&layout.position_map.snapshot.display_snapshot);
+                    let end_display = Point::new(hunk.buffer_range.end, 0)
+                        .to_display_point(&layout.position_map.snapshot.display_snapshot);
+                    let start_y = y_for_row(start_display.row() as f32);
+                    let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
+                        y_for_row((end_display.row() + 1) as f32)
+                    } else {
+                        y_for_row((end_display.row()) as f32)
+                    };
+
+                    if end_y - start_y < 1. {
+                        end_y = start_y + 1.;
+                    }
+                    let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
+
+                    let color = match hunk.status() {
+                        DiffHunkStatus::Added => diff_style.inserted,
+                        DiffHunkStatus::Modified => diff_style.modified,
+                        DiffHunkStatus::Removed => diff_style.deleted,
+                    };
+
+                    let border = Border {
+                        width: 1.,
+                        color: style.thumb.border.color,
+                        overlay: false,
+                        top: false,
+                        right: true,
+                        bottom: false,
+                        left: true,
+                    };
+
+                    cx.scene().push_quad(Quad {
+                        bounds,
+                        background: Some(color),
+                        border: border.into(),
+                        corner_radii: style.thumb.corner_radii.into(),
+                    })
+                }
+            }
+
+            cx.scene().push_quad(Quad {
+                bounds: thumb_bounds,
+                border: style.thumb.border.into(),
+                background: style.thumb.background_color,
+                corner_radii: style.thumb.corner_radii.into(),
+            });
+        }
+
+        cx.scene().push_cursor_region(CursorRegion {
+            bounds: track_bounds,
+            style: CursorStyle::Arrow,
+        });
+        let region_id = cx.view_id();
+        cx.scene().push_mouse_region(
+            MouseRegion::new::<ScrollbarMouseHandlers>(region_id, region_id, track_bounds)
+                .on_move(move |event, editor: &mut Editor, cx| {
+                    if event.pressed_button.is_none() {
+                        editor.scroll_manager.show_scrollbar(cx);
+                    }
+                })
+                .on_down(MouseButton::Left, {
+                    let row_range = row_range.clone();
+                    move |event, editor: &mut Editor, cx| {
+                        let y = event.position.y();
+                        if y < thumb_top || thumb_bottom < y {
+                            let center_row = ((y - top) * max_row as f32 / height).round() as u32;
+                            let top_row = center_row
+                                .saturating_sub((row_range.end - row_range.start) as u32 / 2);
+                            let mut position = editor.scroll_position(cx);
+                            position.set_y(top_row as f32);
+                            editor.set_scroll_position(position, cx);
+                        } else {
+                            editor.scroll_manager.show_scrollbar(cx);
+                        }
+                    }
+                })
+                .on_drag(MouseButton::Left, {
+                    move |event, editor: &mut Editor, cx| {
+                        if event.end {
+                            return;
+                        }
+
+                        let y = event.prev_mouse_position.y();
+                        let new_y = event.position.y();
+                        if thumb_top < y && y < thumb_bottom {
+                            let mut position = editor.scroll_position(cx);
+                            position.set_y(position.y() + (new_y - y) * (max_row as f32) / height);
+                            if position.y() < 0.0 {
+                                position.set_y(0.);
+                            }
+                            editor.set_scroll_position(position, cx);
+                        }
+                    }
+                }),
+        );
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    fn paint_highlighted_range(
+        &self,
+        range: Range<DisplayPoint>,
+        color: Color,
+        corner_radius: f32,
+        line_end_overshoot: f32,
+        layout: &LayoutState,
+        content_origin: Vector2F,
+        scroll_top: f32,
+        scroll_left: f32,
+        bounds: RectF,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let start_row = layout.visible_display_row_range.start;
+        let end_row = layout.visible_display_row_range.end;
+        if range.start != range.end {
+            let row_range = if range.end.column() == 0 {
+                cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
+            } else {
+                cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
+            };
+
+            let highlighted_range = HighlightedRange {
+                color,
+                line_height: layout.position_map.line_height,
+                corner_radius,
+                start_y: content_origin.y()
+                    + row_range.start as f32 * layout.position_map.line_height
+                    - scroll_top,
+                lines: row_range
+                    .into_iter()
+                    .map(|row| {
+                        let line_layout =
+                            &layout.position_map.line_layouts[(row - start_row) as usize].line;
+                        HighlightedRangeLine {
+                            start_x: if row == range.start.row() {
+                                content_origin.x()
+                                    + line_layout.x_for_index(range.start.column() as usize)
+                                    - scroll_left
+                            } else {
+                                content_origin.x() - scroll_left
+                            },
+                            end_x: if row == range.end.row() {
+                                content_origin.x()
+                                    + line_layout.x_for_index(range.end.column() as usize)
+                                    - scroll_left
+                            } else {
+                                content_origin.x() + line_layout.width() + line_end_overshoot
+                                    - scroll_left
+                            },
+                        }
+                    })
+                    .collect(),
+            };
+
+            highlighted_range.paint(bounds, cx);
+        }
+    }
+
+    fn paint_blocks(
+        &mut self,
+        bounds: RectF,
+        visible_bounds: RectF,
+        layout: &mut LayoutState,
+        editor: &mut Editor,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let scroll_position = layout.position_map.snapshot.scroll_position();
+        let scroll_left = scroll_position.x() * layout.position_map.em_width;
+        let scroll_top = scroll_position.y() * layout.position_map.line_height;
+
+        for block in &mut layout.blocks {
+            let mut origin = bounds.origin()
+                + vec2f(
+                    0.,
+                    block.row as f32 * layout.position_map.line_height - scroll_top,
+                );
+            if !matches!(block.style, BlockStyle::Sticky) {
+                origin += vec2f(-scroll_left, 0.);
+            }
+            block.element.paint(origin, visible_bounds, editor, cx);
+        }
+    }
+
+    fn column_pixels(&self, column: usize, cx: &ViewContext<Editor>) -> f32 {
+        let style = &self.style;
+
+        cx.text_layout_cache()
+            .layout_str(
+                " ".repeat(column).as_str(),
+                style.text.font_size,
+                &[(
+                    column,
+                    RunStyle {
+                        font_id: style.text.font_id,
+                        color: Color::black(),
+                        underline: Default::default(),
+                    },
+                )],
+            )
+            .width()
+    }
+
+    fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> f32 {
+        let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1;
+        self.column_pixels(digit_count, cx)
+    }
+
+    //Folds contained in a hunk are ignored apart from shrinking visual size
+    //If a fold contains any hunks then that fold line is marked as modified
+    fn layout_git_gutters(
+        &self,
+        display_rows: Range<u32>,
+        snapshot: &EditorSnapshot,
+    ) -> Vec<DisplayDiffHunk> {
+        let buffer_snapshot = &snapshot.buffer_snapshot;
+
+        let buffer_start_row = DisplayPoint::new(display_rows.start, 0)
+            .to_point(snapshot)
+            .row;
+        let buffer_end_row = DisplayPoint::new(display_rows.end, 0)
+            .to_point(snapshot)
+            .row;
+
+        buffer_snapshot
+            .git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
+            .map(|hunk| diff_hunk_to_display(hunk, snapshot))
+            .dedup()
+            .collect()
+    }
+
+    fn calculate_relative_line_numbers(
+        &self,
+        snapshot: &EditorSnapshot,
+        rows: &Range<u32>,
+        relative_to: Option<u32>,
+    ) -> HashMap<u32, u32> {
+        let mut relative_rows: HashMap<u32, u32> = Default::default();
+        let Some(relative_to) = relative_to else {
+            return relative_rows;
+        };
+
+        let start = rows.start.min(relative_to);
+        let end = rows.end.max(relative_to);
+
+        let buffer_rows = snapshot
+            .buffer_rows(start)
+            .take(1 + (end - start) as usize)
+            .collect::<Vec<_>>();
+
+        let head_idx = relative_to - start;
+        let mut delta = 1;
+        let mut i = head_idx + 1;
+        while i < buffer_rows.len() as u32 {
+            if buffer_rows[i as usize].is_some() {
+                if rows.contains(&(i + start)) {
+                    relative_rows.insert(i + start, delta);
+                }
+                delta += 1;
+            }
+            i += 1;
+        }
+        delta = 1;
+        i = head_idx.min(buffer_rows.len() as u32 - 1);
+        while i > 0 && buffer_rows[i as usize].is_none() {
+            i -= 1;
+        }
+
+        while i > 0 {
+            i -= 1;
+            if buffer_rows[i as usize].is_some() {
+                if rows.contains(&(i + start)) {
+                    relative_rows.insert(i + start, delta);
+                }
+                delta += 1;
+            }
+        }
+
+        relative_rows
+    }
+
+    fn layout_line_numbers(
+        &self,
+        rows: Range<u32>,
+        active_rows: &BTreeMap<u32, bool>,
+        newest_selection_head: DisplayPoint,
+        is_singleton: bool,
+        snapshot: &EditorSnapshot,
+        cx: &ViewContext<Editor>,
+    ) -> (
+        Vec<Option<text_layout::Line>>,
+        Vec<Option<(FoldStatus, BufferRow, bool)>>,
+    ) {
+        let style = &self.style;
+        let include_line_numbers = snapshot.mode == EditorMode::Full;
+        let mut line_number_layouts = Vec::with_capacity(rows.len());
+        let mut fold_statuses = Vec::with_capacity(rows.len());
+        let mut line_number = String::new();
+        let is_relative = settings::get::<EditorSettings>(cx).relative_line_numbers;
+        let relative_to = if is_relative {
+            Some(newest_selection_head.row())
+        } else {
+            None
+        };
+
+        let relative_rows = self.calculate_relative_line_numbers(&snapshot, &rows, relative_to);
+
+        for (ix, row) in snapshot
+            .buffer_rows(rows.start)
+            .take((rows.end - rows.start) as usize)
+            .enumerate()
+        {
+            let display_row = rows.start + ix as u32;
+            let (active, color) = if active_rows.contains_key(&display_row) {
+                (true, style.line_number_active)
+            } else {
+                (false, style.line_number)
+            };
+            if let Some(buffer_row) = row {
+                if include_line_numbers {
+                    line_number.clear();
+                    let default_number = buffer_row + 1;
+                    let number = relative_rows
+                        .get(&(ix as u32 + rows.start))
+                        .unwrap_or(&default_number);
+                    write!(&mut line_number, "{}", number).unwrap();
+                    line_number_layouts.push(Some(cx.text_layout_cache().layout_str(
+                        &line_number,
+                        style.text.font_size,
+                        &[(
+                            line_number.len(),
+                            RunStyle {
+                                font_id: style.text.font_id,
+                                color,
+                                underline: Default::default(),
+                            },
+                        )],
+                    )));
+                    fold_statuses.push(
+                        is_singleton
+                            .then(|| {
+                                snapshot
+                                    .fold_for_line(buffer_row)
+                                    .map(|fold_status| (fold_status, buffer_row, active))
+                            })
+                            .flatten(),
+                    )
+                }
+            } else {
+                fold_statuses.push(None);
+                line_number_layouts.push(None);
+            }
+        }
+
+        (line_number_layouts, fold_statuses)
+    }
+
+    fn layout_lines(
+        &mut self,
+        rows: Range<u32>,
+        line_number_layouts: &[Option<Line>],
+        snapshot: &EditorSnapshot,
+        cx: &ViewContext<Editor>,
+    ) -> Vec<LineWithInvisibles> {
+        if rows.start >= rows.end {
+            return Vec::new();
+        }
+
+        // When the editor is empty and unfocused, then show the placeholder.
+        if snapshot.is_empty() {
+            let placeholder_style = self
+                .style
+                .placeholder_text
+                .as_ref()
+                .unwrap_or(&self.style.text);
+            let placeholder_text = snapshot.placeholder_text();
+            let placeholder_lines = placeholder_text
+                .as_ref()
+                .map_or("", AsRef::as_ref)
+                .split('\n')
+                .skip(rows.start as usize)
+                .chain(iter::repeat(""))
+                .take(rows.len());
+            placeholder_lines
+                .map(|line| {
+                    cx.text_layout_cache().layout_str(
+                        line,
+                        placeholder_style.font_size,
+                        &[(
+                            line.len(),
+                            RunStyle {
+                                font_id: placeholder_style.font_id,
+                                color: placeholder_style.color,
+                                underline: Default::default(),
+                            },
+                        )],
+                    )
+                })
+                .map(|line| LineWithInvisibles {
+                    line,
+                    invisibles: Vec::new(),
+                })
+                .collect()
+        } else {
+            let style = &self.style;
+            let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
+
+            LineWithInvisibles::from_chunks(
+                chunks,
+                &style.text,
+                cx.text_layout_cache(),
+                cx.font_cache(),
+                MAX_LINE_LEN,
+                rows.len() as usize,
+                line_number_layouts,
+                snapshot.mode,
+            )
+        }
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    fn layout_blocks(
+        &mut self,
+        rows: Range<u32>,
+        snapshot: &EditorSnapshot,
+        editor_width: f32,
+        scroll_width: f32,
+        gutter_padding: f32,
+        gutter_width: f32,
+        em_width: f32,
+        text_x: f32,
+        line_height: f32,
+        style: &EditorStyle,
+        line_layouts: &[LineWithInvisibles],
+        editor: &mut Editor,
+        cx: &mut ViewContext<Editor>,
+    ) -> (f32, Vec<BlockLayout>) {
+        let mut block_id = 0;
+        let scroll_x = snapshot.scroll_anchor.offset.x();
+        let (fixed_blocks, non_fixed_blocks) = snapshot
+            .blocks_in_range(rows.clone())
+            .partition::<Vec<_>, _>(|(_, block)| match block {
+                TransformBlock::ExcerptHeader { .. } => false,
+                TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
+            });
+        let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| {
+            let mut element = match block {
+                TransformBlock::Custom(block) => {
+                    let align_to = block
+                        .position()
+                        .to_point(&snapshot.buffer_snapshot)
+                        .to_display_point(snapshot);
+                    let anchor_x = text_x
+                        + if rows.contains(&align_to.row()) {
+                            line_layouts[(align_to.row() - rows.start) as usize]
+                                .line
+                                .x_for_index(align_to.column() as usize)
+                        } else {
+                            layout_line(align_to.row(), snapshot, style, cx.text_layout_cache())
+                                .x_for_index(align_to.column() as usize)
+                        };
+
+                    block.render(&mut BlockContext {
+                        view_context: cx,
+                        anchor_x,
+                        gutter_padding,
+                        line_height,
+                        scroll_x,
+                        gutter_width,
+                        em_width,
+                        block_id,
+                    })
+                }
+                TransformBlock::ExcerptHeader {
+                    id,
+                    buffer,
+                    range,
+                    starts_new_buffer,
+                    ..
+                } => {
+                    let tooltip_style = theme::current(cx).tooltip.clone();
+                    let include_root = editor
+                        .project
+                        .as_ref()
+                        .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+                        .unwrap_or_default();
+                    let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
+                        let jump_path = ProjectPath {
+                            worktree_id: file.worktree_id(cx),
+                            path: file.path.clone(),
+                        };
+                        let jump_anchor = range
+                            .primary
+                            .as_ref()
+                            .map_or(range.context.start, |primary| primary.start);
+                        let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
+
+                        enum JumpIcon {}
+                        MouseEventHandler::new::<JumpIcon, _>((*id).into(), cx, |state, _| {
+                            let style = style.jump_icon.style_for(state);
+                            Svg::new("icons/arrow_up_right.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)
+                        })
+                        .with_cursor_style(CursorStyle::PointingHand)
+                        .on_click(MouseButton::Left, move |_, editor, cx| {
+                            if let Some(workspace) = editor
+                                .workspace
+                                .as_ref()
+                                .and_then(|(workspace, _)| workspace.upgrade(cx))
+                            {
+                                workspace.update(cx, |workspace, cx| {
+                                    Editor::jump(
+                                        workspace,
+                                        jump_path.clone(),
+                                        jump_position,
+                                        jump_anchor,
+                                        cx,
+                                    );
+                                });
+                            }
+                        })
+                        .with_tooltip::<JumpIcon>(
+                            (*id).into(),
+                            "Jump to Buffer".to_string(),
+                            Some(Box::new(crate::OpenExcerpts)),
+                            tooltip_style.clone(),
+                            cx,
+                        )
+                        .aligned()
+                        .flex_float()
+                    });
+
+                    if *starts_new_buffer {
+                        let editor_font_size = style.text.font_size;
+                        let style = &style.diagnostic_path_header;
+                        let font_size = (style.text_scale_factor * editor_font_size).round();
+
+                        let path = buffer.resolve_file_path(cx, include_root);
+                        let mut filename = None;
+                        let mut parent_path = None;
+                        // Can't use .and_then() because `.file_name()` and `.parent()` return references :(
+                        if let Some(path) = path {
+                            filename = path.file_name().map(|f| f.to_string_lossy().to_string());
+                            parent_path =
+                                path.parent().map(|p| p.to_string_lossy().to_string() + "/");
+                        }
+
+                        Flex::row()
+                            .with_child(
+                                Label::new(
+                                    filename.unwrap_or_else(|| "untitled".to_string()),
+                                    style.filename.text.clone().with_font_size(font_size),
+                                )
+                                .contained()
+                                .with_style(style.filename.container)
+                                .aligned(),
+                            )
+                            .with_children(parent_path.map(|path| {
+                                Label::new(path, style.path.text.clone().with_font_size(font_size))
+                                    .contained()
+                                    .with_style(style.path.container)
+                                    .aligned()
+                            }))
+                            .with_children(jump_icon)
+                            .contained()
+                            .with_style(style.container)
+                            .with_padding_left(gutter_padding)
+                            .with_padding_right(gutter_padding)
+                            .expanded()
+                            .into_any_named("path header block")
+                    } else {
+                        let text_style = style.text.clone();
+                        Flex::row()
+                            .with_child(Label::new("โ‹ฏ", text_style))
+                            .with_children(jump_icon)
+                            .contained()
+                            .with_padding_left(gutter_padding)
+                            .with_padding_right(gutter_padding)
+                            .expanded()
+                            .into_any_named("collapsed context")
+                    }
+                }
+            };
+
+            element.layout(
+                SizeConstraint {
+                    min: Vector2F::zero(),
+                    max: vec2f(width, block.height() as f32 * line_height),
+                },
+                editor,
+                cx,
+            );
+            element
+        };
+
+        let mut fixed_block_max_width = 0f32;
+        let mut blocks = Vec::new();
+        for (row, block) in fixed_blocks {
+            let element = render_block(block, f32::INFINITY, block_id);
+            block_id += 1;
+            fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width);
+            blocks.push(BlockLayout {
+                row,
+                element,
+                style: BlockStyle::Fixed,
+            });
+        }
+        for (row, block) in non_fixed_blocks {
+            let style = match block {
+                TransformBlock::Custom(block) => block.style(),
+                TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky,
+            };
+            let width = match style {
+                BlockStyle::Sticky => editor_width,
+                BlockStyle::Flex => editor_width
+                    .max(fixed_block_max_width)
+                    .max(gutter_width + scroll_width),
+                BlockStyle::Fixed => unreachable!(),
+            };
+            let element = render_block(block, width, block_id);
+            block_id += 1;
+            blocks.push(BlockLayout {
+                row,
+                element,
+                style,
+            });
+        }
+        (
+            scroll_width.max(fixed_block_max_width - gutter_width),
+            blocks,
+        )
+    }
+}
+
+#[derive(Debug)]
+pub struct LineWithInvisibles {
+    pub line: Line,
+    invisibles: Vec<Invisible>,
+}
+
+impl LineWithInvisibles {
+    fn from_chunks<'a>(
+        chunks: impl Iterator<Item = HighlightedChunk<'a>>,
+        text_style: &TextStyle,
+        text_layout_cache: &TextLayoutCache,
+        font_cache: &Arc<FontCache>,
+        max_line_len: usize,
+        max_line_count: usize,
+        line_number_layouts: &[Option<Line>],
+        editor_mode: EditorMode,
+    ) -> Vec<Self> {
+        let mut layouts = Vec::with_capacity(max_line_count);
+        let mut line = String::new();
+        let mut invisibles = Vec::new();
+        let mut styles = Vec::new();
+        let mut non_whitespace_added = false;
+        let mut row = 0;
+        let mut line_exceeded_max_len = false;
+        for highlighted_chunk in chunks.chain([HighlightedChunk {
+            chunk: "\n",
+            style: None,
+            is_tab: false,
+        }]) {
+            for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
+                if ix > 0 {
+                    layouts.push(Self {
+                        line: text_layout_cache.layout_str(&line, text_style.font_size, &styles),
+                        invisibles: invisibles.drain(..).collect(),
+                    });
+
+                    line.clear();
+                    styles.clear();
+                    row += 1;
+                    line_exceeded_max_len = false;
+                    non_whitespace_added = false;
+                    if row == max_line_count {
+                        return layouts;
+                    }
+                }
+
+                if !line_chunk.is_empty() && !line_exceeded_max_len {
+                    let text_style = if let Some(style) = highlighted_chunk.style {
+                        text_style
+                            .clone()
+                            .highlight(style, font_cache)
+                            .map(Cow::Owned)
+                            .unwrap_or_else(|_| Cow::Borrowed(text_style))
+                    } else {
+                        Cow::Borrowed(text_style)
+                    };
+
+                    if line.len() + line_chunk.len() > max_line_len {
+                        let mut chunk_len = max_line_len - line.len();
+                        while !line_chunk.is_char_boundary(chunk_len) {
+                            chunk_len -= 1;
+                        }
+                        line_chunk = &line_chunk[..chunk_len];
+                        line_exceeded_max_len = true;
+                    }
+
+                    styles.push((
+                        line_chunk.len(),
+                        RunStyle {
+                            font_id: text_style.font_id,
+                            color: text_style.color,
+                            underline: text_style.underline,
+                        },
+                    ));
+
+                    if editor_mode == EditorMode::Full {
+                        // Line wrap pads its contents with fake whitespaces,
+                        // avoid printing them
+                        let inside_wrapped_string = line_number_layouts
+                            .get(row)
+                            .and_then(|layout| layout.as_ref())
+                            .is_none();
+                        if highlighted_chunk.is_tab {
+                            if non_whitespace_added || !inside_wrapped_string {
+                                invisibles.push(Invisible::Tab {
+                                    line_start_offset: line.len(),
+                                });
+                            }
+                        } else {
+                            invisibles.extend(
+                                line_chunk
+                                    .chars()
+                                    .enumerate()
+                                    .filter(|(_, line_char)| {
+                                        let is_whitespace = line_char.is_whitespace();
+                                        non_whitespace_added |= !is_whitespace;
+                                        is_whitespace
+                                            && (non_whitespace_added || !inside_wrapped_string)
+                                    })
+                                    .map(|(whitespace_index, _)| Invisible::Whitespace {
+                                        line_offset: line.len() + whitespace_index,
+                                    }),
+                            )
+                        }
+                    }
+
+                    line.push_str(line_chunk);
+                }
+            }
+        }
+
+        layouts
+    }
+
+    fn draw(
+        &self,
+        layout: &LayoutState,
+        row: u32,
+        scroll_top: f32,
+        content_origin: Vector2F,
+        scroll_left: f32,
+        visible_text_bounds: RectF,
+        whitespace_setting: ShowWhitespaceSetting,
+        selection_ranges: &[Range<DisplayPoint>],
+        visible_bounds: RectF,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let line_height = layout.position_map.line_height;
+        let line_y = row as f32 * line_height - scroll_top;
+
+        self.line.paint(
+            content_origin + vec2f(-scroll_left, line_y),
+            visible_text_bounds,
+            line_height,
+            cx,
+        );
+
+        self.draw_invisibles(
+            &selection_ranges,
+            layout,
+            content_origin,
+            scroll_left,
+            line_y,
+            row,
+            visible_bounds,
+            line_height,
+            whitespace_setting,
+            cx,
+        );
+    }
+
+    fn draw_invisibles(
+        &self,
+        selection_ranges: &[Range<DisplayPoint>],
+        layout: &LayoutState,
+        content_origin: Vector2F,
+        scroll_left: f32,
+        line_y: f32,
+        row: u32,
+        visible_bounds: RectF,
+        line_height: f32,
+        whitespace_setting: ShowWhitespaceSetting,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let allowed_invisibles_regions = match whitespace_setting {
+            ShowWhitespaceSetting::None => return,
+            ShowWhitespaceSetting::Selection => Some(selection_ranges),
+            ShowWhitespaceSetting::All => None,
+        };
+
+        for invisible in &self.invisibles {
+            let (&token_offset, invisible_symbol) = match invisible {
+                Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible),
+                Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible),
+            };
+
+            let x_offset = self.line.x_for_index(token_offset);
+            let invisible_offset =
+                (layout.position_map.em_width - invisible_symbol.width()).max(0.0) / 2.0;
+            let origin = content_origin + vec2f(-scroll_left + x_offset + invisible_offset, line_y);
+
+            if let Some(allowed_regions) = allowed_invisibles_regions {
+                let invisible_point = DisplayPoint::new(row, token_offset as u32);
+                if !allowed_regions
+                    .iter()
+                    .any(|region| region.start <= invisible_point && invisible_point < region.end)
+                {
+                    continue;
+                }
+            }
+            invisible_symbol.paint(origin, visible_bounds, line_height, cx);
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum Invisible {
+    Tab { line_start_offset: usize },
+    Whitespace { line_offset: usize },
+}
+
+impl Element<Editor> for EditorElement {
+    type LayoutState = LayoutState;
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: SizeConstraint,
+        editor: &mut Editor,
+        cx: &mut ViewContext<Editor>,
+    ) -> (Vector2F, Self::LayoutState) {
+        let mut size = constraint.max;
+        if size.x().is_infinite() {
+            unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
+        }
+
+        let snapshot = editor.snapshot(cx);
+        let style = self.style.clone();
+
+        let line_height = (style.text.font_size * style.line_height_scalar).round();
+
+        let gutter_padding;
+        let gutter_width;
+        let gutter_margin;
+        if snapshot.show_gutter {
+            let em_width = style.text.em_width(cx.font_cache());
+            gutter_padding = (em_width * style.gutter_padding_factor).round();
+            gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
+            gutter_margin = -style.text.descent(cx.font_cache());
+        } else {
+            gutter_padding = 0.0;
+            gutter_width = 0.0;
+            gutter_margin = 0.0;
+        };
+
+        let text_width = size.x() - gutter_width;
+        let em_width = style.text.em_width(cx.font_cache());
+        let em_advance = style.text.em_advance(cx.font_cache());
+        let overscroll = vec2f(em_width, 0.);
+        let snapshot = {
+            editor.set_visible_line_count(size.y() / line_height, cx);
+
+            let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
+            let wrap_width = match editor.soft_wrap_mode(cx) {
+                SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
+                SoftWrap::EditorWidth => editor_width,
+                SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
+            };
+
+            if editor.set_wrap_width(Some(wrap_width), cx) {
+                editor.snapshot(cx)
+            } else {
+                snapshot
+            }
+        };
+
+        let wrap_guides = editor
+            .wrap_guides(cx)
+            .iter()
+            .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
+            .collect();
+
+        let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height;
+        if let EditorMode::AutoHeight { max_lines } = snapshot.mode {
+            size.set_y(
+                scroll_height
+                    .min(constraint.max_along(Axis::Vertical))
+                    .max(constraint.min_along(Axis::Vertical))
+                    .max(line_height)
+                    .min(line_height * max_lines as f32),
+            )
+        } else if let EditorMode::SingleLine = snapshot.mode {
+            size.set_y(line_height.max(constraint.min_along(Axis::Vertical)))
+        } else if size.y().is_infinite() {
+            size.set_y(scroll_height);
+        }
+        let gutter_size = vec2f(gutter_width, size.y());
+        let text_size = vec2f(text_width, size.y());
+
+        let autoscroll_horizontally = editor.autoscroll_vertically(size.y(), line_height, cx);
+        let mut snapshot = editor.snapshot(cx);
+
+        let scroll_position = snapshot.scroll_position();
+        // The scroll position is a fractional point, the whole number of which represents
+        // the top of the window in terms of display rows.
+        let start_row = scroll_position.y() as u32;
+        let height_in_lines = size.y() / line_height;
+        let max_row = snapshot.max_point().row();
+
+        // Add 1 to ensure selections bleed off screen
+        let end_row = 1 + cmp::min(
+            (scroll_position.y() + height_in_lines).ceil() as u32,
+            max_row,
+        );
+
+        let start_anchor = if start_row == 0 {
+            Anchor::min()
+        } else {
+            snapshot
+                .buffer_snapshot
+                .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
+        };
+        let end_anchor = if end_row > max_row {
+            Anchor::max()
+        } else {
+            snapshot
+                .buffer_snapshot
+                .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
+        };
+
+        let mut selections: Vec<(SelectionStyle, Vec<SelectionLayout>)> = Vec::new();
+        let mut active_rows = BTreeMap::new();
+        let mut fold_ranges = Vec::new();
+        let is_singleton = editor.is_singleton(cx);
+
+        let highlighted_rows = editor.highlighted_rows();
+        let theme = theme::current(cx);
+        let highlighted_ranges = editor.background_highlights_in_range(
+            start_anchor..end_anchor,
+            &snapshot.display_snapshot,
+            theme.as_ref(),
+        );
+
+        fold_ranges.extend(
+            snapshot
+                .folds_in_range(start_anchor..end_anchor)
+                .map(|anchor| {
+                    let start = anchor.start.to_point(&snapshot.buffer_snapshot);
+                    (
+                        start.row,
+                        start.to_display_point(&snapshot.display_snapshot)
+                            ..anchor.end.to_display_point(&snapshot),
+                    )
+                }),
+        );
+
+        let mut newest_selection_head = None;
+
+        if editor.show_local_selections {
+            let mut local_selections: Vec<Selection<Point>> = editor
+                .selections
+                .disjoint_in_range(start_anchor..end_anchor, cx);
+            local_selections.extend(editor.selections.pending(cx));
+            let mut layouts = Vec::new();
+            let newest = editor.selections.newest(cx);
+            for selection in local_selections.drain(..) {
+                let is_empty = selection.start == selection.end;
+                let is_newest = selection == newest;
+
+                let layout = SelectionLayout::new(
+                    selection,
+                    editor.selections.line_mode,
+                    editor.cursor_shape,
+                    &snapshot.display_snapshot,
+                    is_newest,
+                    true,
+                );
+                if is_newest {
+                    newest_selection_head = Some(layout.head);
+                }
+
+                for row in cmp::max(layout.active_rows.start, start_row)
+                    ..=cmp::min(layout.active_rows.end, end_row)
+                {
+                    let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
+                    *contains_non_empty_selection |= !is_empty;
+                }
+                layouts.push(layout);
+            }
+
+            selections.push((style.selection, layouts));
+        }
+
+        if let Some(collaboration_hub) = &editor.collaboration_hub {
+            // When following someone, render the local selections in their color.
+            if let Some(leader_id) = editor.leader_peer_id {
+                if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
+                    if let Some(participant_index) = collaboration_hub
+                        .user_participant_indices(cx)
+                        .get(&collaborator.user_id)
+                    {
+                        if let Some((local_selection_style, _)) = selections.first_mut() {
+                            *local_selection_style =
+                                style.selection_style_for_room_participant(participant_index.0);
+                        }
+                    }
+                }
+            }
+
+            let mut remote_selections = HashMap::default();
+            for selection in snapshot.remote_selections_in_range(
+                &(start_anchor..end_anchor),
+                collaboration_hub.as_ref(),
+                cx,
+            ) {
+                let selection_style = if let Some(participant_index) = selection.participant_index {
+                    style.selection_style_for_room_participant(participant_index.0)
+                } else {
+                    style.absent_selection
+                };
+
+                // Don't re-render the leader's selections, since the local selections
+                // match theirs.
+                if Some(selection.peer_id) == editor.leader_peer_id {
+                    continue;
+                }
+
+                remote_selections
+                    .entry(selection.replica_id)
+                    .or_insert((selection_style, Vec::new()))
+                    .1
+                    .push(SelectionLayout::new(
+                        selection.selection,
+                        selection.line_mode,
+                        selection.cursor_shape,
+                        &snapshot.display_snapshot,
+                        false,
+                        false,
+                    ));
+            }
+
+            selections.extend(remote_selections.into_values());
+        }
+
+        let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
+        let show_scrollbars = match scrollbar_settings.show {
+            ShowScrollbar::Auto => {
+                // Git
+                (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
+                ||
+                // Selections
+                (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty())
+                // Scrollmanager
+                || editor.scroll_manager.scrollbars_visible()
+            }
+            ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
+            ShowScrollbar::Always => true,
+            ShowScrollbar::Never => false,
+        };
+
+        let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
+            .into_iter()
+            .map(|(id, fold)| {
+                let color = self
+                    .style
+                    .folds
+                    .ellipses
+                    .background
+                    .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
+                    .color;
+
+                (id, fold, color)
+            })
+            .collect();
+
+        let head_for_relative = newest_selection_head.unwrap_or_else(|| {
+            let newest = editor.selections.newest::<Point>(cx);
+            SelectionLayout::new(
+                newest,
+                editor.selections.line_mode,
+                editor.cursor_shape,
+                &snapshot.display_snapshot,
+                true,
+                true,
+            )
+            .head
+        });
+
+        let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
+            start_row..end_row,
+            &active_rows,
+            head_for_relative,
+            is_singleton,
+            &snapshot,
+            cx,
+        );
+
+        let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
+
+        let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
+
+        let mut max_visible_line_width = 0.0;
+        let line_layouts =
+            self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
+        for line_with_invisibles in &line_layouts {
+            if line_with_invisibles.line.width() > max_visible_line_width {
+                max_visible_line_width = line_with_invisibles.line.width();
+            }
+        }
+
+        let style = self.style.clone();
+        let longest_line_width = layout_line(
+            snapshot.longest_row(),
+            &snapshot,
+            &style,
+            cx.text_layout_cache(),
+        )
+        .width();
+        let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x();
+        let em_width = style.text.em_width(cx.font_cache());
+        let (scroll_width, blocks) = self.layout_blocks(
+            start_row..end_row,
+            &snapshot,
+            size.x(),
+            scroll_width,
+            gutter_padding,
+            gutter_width,
+            em_width,
+            gutter_width + gutter_margin,
+            line_height,
+            &style,
+            &line_layouts,
+            editor,
+            cx,
+        );
+
+        let scroll_max = vec2f(
+            ((scroll_width - text_size.x()) / em_width).max(0.0),
+            max_row as f32,
+        );
+
+        let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x());
+
+        let autoscrolled = if autoscroll_horizontally {
+            editor.autoscroll_horizontally(
+                start_row,
+                text_size.x(),
+                scroll_width,
+                em_width,
+                &line_layouts,
+                cx,
+            )
+        } else {
+            false
+        };
+
+        if clamped || autoscrolled {
+            snapshot = editor.snapshot(cx);
+        }
+
+        let style = editor.style(cx);
+
+        let mut context_menu = None;
+        let mut code_actions_indicator = None;
+        if let Some(newest_selection_head) = newest_selection_head {
+            if (start_row..end_row).contains(&newest_selection_head.row()) {
+                if editor.context_menu_visible() {
+                    context_menu =
+                        editor.render_context_menu(newest_selection_head, style.clone(), cx);
+                }
+
+                let active = matches!(
+                    editor.context_menu.read().as_ref(),
+                    Some(crate::ContextMenu::CodeActions(_))
+                );
+
+                code_actions_indicator = editor
+                    .render_code_actions_indicator(&style, active, cx)
+                    .map(|indicator| (newest_selection_head.row(), indicator));
+            }
+        }
+
+        let visible_rows = start_row..start_row + line_layouts.len() as u32;
+        let mut hover = editor.hover_state.render(
+            &snapshot,
+            &style,
+            visible_rows,
+            editor.workspace.as_ref().map(|(w, _)| w.clone()),
+            cx,
+        );
+        let mode = editor.mode;
+
+        let mut fold_indicators = editor.render_fold_indicators(
+            fold_statuses,
+            &style,
+            editor.gutter_hovered,
+            line_height,
+            gutter_margin,
+            cx,
+        );
+
+        if let Some((_, context_menu)) = context_menu.as_mut() {
+            context_menu.layout(
+                SizeConstraint {
+                    min: Vector2F::zero(),
+                    max: vec2f(
+                        cx.window_size().x() * 0.7,
+                        (12. * line_height).min((size.y() - line_height) / 2.),
+                    ),
+                },
+                editor,
+                cx,
+            );
+        }
+
+        if let Some((_, indicator)) = code_actions_indicator.as_mut() {
+            indicator.layout(
+                SizeConstraint::strict_along(
+                    Axis::Vertical,
+                    line_height * style.code_actions.vertical_scale,
+                ),
+                editor,
+                cx,
+            );
+        }
+
+        for fold_indicator in fold_indicators.iter_mut() {
+            if let Some(indicator) = fold_indicator.as_mut() {
+                indicator.layout(
+                    SizeConstraint::strict_along(
+                        Axis::Vertical,
+                        line_height * style.code_actions.vertical_scale,
+                    ),
+                    editor,
+                    cx,
+                );
+            }
+        }
+
+        if let Some((_, hover_popovers)) = hover.as_mut() {
+            for hover_popover in hover_popovers.iter_mut() {
+                hover_popover.layout(
+                    SizeConstraint {
+                        min: Vector2F::zero(),
+                        max: vec2f(
+                            (120. * em_width) // Default size
+                                .min(size.x() / 2.) // Shrink to half of the editor width
+                                .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
+                            (16. * line_height) // Default size
+                                .min(size.y() / 2.) // Shrink to half of the editor height
+                                .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
+                        ),
+                    },
+                    editor,
+                    cx,
+                );
+            }
+        }
+
+        let invisible_symbol_font_size = self.style.text.font_size / 2.0;
+        let invisible_symbol_style = RunStyle {
+            color: self.style.whitespace,
+            font_id: self.style.text.font_id,
+            underline: Default::default(),
+        };
+
+        (
+            size,
+            LayoutState {
+                mode,
+                position_map: Arc::new(PositionMap {
+                    size,
+                    scroll_max,
+                    line_layouts,
+                    line_height,
+                    em_width,
+                    em_advance,
+                    snapshot,
+                }),
+                visible_display_row_range: start_row..end_row,
+                wrap_guides,
+                gutter_size,
+                gutter_padding,
+                text_size,
+                scrollbar_row_range,
+                show_scrollbars,
+                is_singleton,
+                max_row,
+                gutter_margin,
+                active_rows,
+                highlighted_rows,
+                highlighted_ranges,
+                fold_ranges,
+                line_number_layouts,
+                display_hunks,
+                blocks,
+                selections,
+                context_menu,
+                code_actions_indicator,
+                fold_indicators,
+                tab_invisible: cx.text_layout_cache().layout_str(
+                    "โ†’",
+                    invisible_symbol_font_size,
+                    &[("โ†’".len(), invisible_symbol_style)],
+                ),
+                space_invisible: cx.text_layout_cache().layout_str(
+                    "โ€ข",
+                    invisible_symbol_font_size,
+                    &[("โ€ข".len(), invisible_symbol_style)],
+                ),
+                hover_popovers: hover,
+            },
+        )
+    }
+
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        visible_bounds: RectF,
+        layout: &mut Self::LayoutState,
+        editor: &mut Editor,
+        cx: &mut ViewContext<Editor>,
+    ) -> Self::PaintState {
+        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+        cx.scene().push_layer(Some(visible_bounds));
+
+        let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
+        let text_bounds = RectF::new(
+            bounds.origin() + vec2f(layout.gutter_size.x(), 0.0),
+            layout.text_size,
+        );
+
+        Self::attach_mouse_handlers(
+            &layout.position_map,
+            layout.hover_popovers.is_some(),
+            visible_bounds,
+            text_bounds,
+            gutter_bounds,
+            bounds,
+            cx,
+        );
+
+        self.paint_background(gutter_bounds, text_bounds, layout, cx);
+        if layout.gutter_size.x() > 0. {
+            self.paint_gutter(gutter_bounds, visible_bounds, layout, editor, cx);
+        }
+        self.paint_text(text_bounds, visible_bounds, layout, editor, cx);
+
+        cx.scene().push_layer(Some(bounds));
+        if !layout.blocks.is_empty() {
+            self.paint_blocks(bounds, visible_bounds, layout, editor, cx);
+        }
+        self.paint_scrollbar(bounds, layout, &editor, cx);
+        cx.scene().pop_layer();
+        cx.scene().pop_layer();
+    }
+
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        bounds: RectF,
+        _: RectF,
+        layout: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &Editor,
+        _: &ViewContext<Editor>,
+    ) -> Option<RectF> {
+        let text_bounds = RectF::new(
+            bounds.origin() + vec2f(layout.gutter_size.x(), 0.0),
+            layout.text_size,
+        );
+        let content_origin = text_bounds.origin() + vec2f(layout.gutter_margin, 0.);
+        let scroll_position = layout.position_map.snapshot.scroll_position();
+        let start_row = scroll_position.y() as u32;
+        let scroll_top = scroll_position.y() * layout.position_map.line_height;
+        let scroll_left = scroll_position.x() * layout.position_map.em_width;
+
+        let range_start = OffsetUtf16(range_utf16.start)
+            .to_display_point(&layout.position_map.snapshot.display_snapshot);
+        if range_start.row() < start_row {
+            return None;
+        }
+
+        let line = &layout
+            .position_map
+            .line_layouts
+            .get((range_start.row() - start_row) as usize)?
+            .line;
+        let range_start_x = line.x_for_index(range_start.column() as usize);
+        let range_start_y = range_start.row() as f32 * layout.position_map.line_height;
+        Some(RectF::new(
+            content_origin
+                + vec2f(
+                    range_start_x,
+                    range_start_y + layout.position_map.line_height,
+                )
+                - vec2f(scroll_left, scroll_top),
+            vec2f(
+                layout.position_map.em_width,
+                layout.position_map.line_height,
+            ),
+        ))
+    }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &Editor,
+        _: &ViewContext<Editor>,
+    ) -> json::Value {
+        json!({
+            "type": "BufferElement",
+            "bounds": bounds.to_json()
+        })
+    }
+}
+
+type BufferRow = u32;
+
+pub struct LayoutState {
+    position_map: Arc<PositionMap>,
+    gutter_size: Vector2F,
+    gutter_padding: f32,
+    gutter_margin: f32,
+    text_size: Vector2F,
+    mode: EditorMode,
+    wrap_guides: SmallVec<[(f32, bool); 2]>,
+    visible_display_row_range: Range<u32>,
+    active_rows: BTreeMap<u32, bool>,
+    highlighted_rows: Option<Range<u32>>,
+    line_number_layouts: Vec<Option<text_layout::Line>>,
+    display_hunks: Vec<DisplayDiffHunk>,
+    blocks: Vec<BlockLayout>,
+    highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
+    fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
+    selections: Vec<(SelectionStyle, Vec<SelectionLayout>)>,
+    scrollbar_row_range: Range<f32>,
+    show_scrollbars: bool,
+    is_singleton: bool,
+    max_row: u32,
+    context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
+    code_actions_indicator: Option<(u32, AnyElement<Editor>)>,
+    hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
+    fold_indicators: Vec<Option<AnyElement<Editor>>>,
+    tab_invisible: Line,
+    space_invisible: Line,
+}
+
+struct PositionMap {
+    size: Vector2F,
+    line_height: f32,
+    scroll_max: Vector2F,
+    em_width: f32,
+    em_advance: f32,
+    line_layouts: Vec<LineWithInvisibles>,
+    snapshot: EditorSnapshot,
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct PointForPosition {
+    pub previous_valid: DisplayPoint,
+    pub next_valid: DisplayPoint,
+    pub exact_unclipped: DisplayPoint,
+    pub column_overshoot_after_line_end: u32,
+}
+
+impl PointForPosition {
+    #[cfg(test)]
+    pub fn valid(valid: DisplayPoint) -> Self {
+        Self {
+            previous_valid: valid,
+            next_valid: valid,
+            exact_unclipped: valid,
+            column_overshoot_after_line_end: 0,
+        }
+    }
+
+    pub fn as_valid(&self) -> Option<DisplayPoint> {
+        if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
+            Some(self.previous_valid)
+        } else {
+            None
+        }
+    }
+}
+
+impl PositionMap {
+    fn point_for_position(&self, text_bounds: RectF, position: Vector2F) -> PointForPosition {
+        let scroll_position = self.snapshot.scroll_position();
+        let position = position - text_bounds.origin();
+        let y = position.y().max(0.0).min(self.size.y());
+        let x = position.x() + (scroll_position.x() * self.em_width);
+        let row = (y / self.line_height + scroll_position.y()) as u32;
+        let (column, x_overshoot_after_line_end) = if let Some(line) = self
+            .line_layouts
+            .get(row as usize - scroll_position.y() as usize)
+            .map(|line_with_spaces| &line_with_spaces.line)
+        {
+            if let Some(ix) = line.index_for_x(x) {
+                (ix as u32, 0.0)
+            } else {
+                (line.len() as u32, 0f32.max(x - line.width()))
+            }
+        } else {
+            (0, x)
+        };
+
+        let mut exact_unclipped = DisplayPoint::new(row, column);
+        let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
+        let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
+
+        let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32;
+        *exact_unclipped.column_mut() += column_overshoot_after_line_end;
+        PointForPosition {
+            previous_valid,
+            next_valid,
+            exact_unclipped,
+            column_overshoot_after_line_end,
+        }
+    }
+}
+
+struct BlockLayout {
+    row: u32,
+    element: AnyElement<Editor>,
+    style: BlockStyle,
+}
+
+fn layout_line(
+    row: u32,
+    snapshot: &EditorSnapshot,
+    style: &EditorStyle,
+    layout_cache: &TextLayoutCache,
+) -> text_layout::Line {
+    let mut line = snapshot.line(row);
+
+    if line.len() > MAX_LINE_LEN {
+        let mut len = MAX_LINE_LEN;
+        while !line.is_char_boundary(len) {
+            len -= 1;
+        }
+
+        line.truncate(len);
+    }
+
+    layout_cache.layout_str(
+        &line,
+        style.text.font_size,
+        &[(
+            snapshot.line_len(row) as usize,
+            RunStyle {
+                font_id: style.text.font_id,
+                color: Color::black(),
+                underline: Default::default(),
+            },
+        )],
+    )
+}
+
+#[derive(Debug)]
+pub struct Cursor {
+    origin: Vector2F,
+    block_width: f32,
+    line_height: f32,
+    color: Color,
+    shape: CursorShape,
+    block_text: Option<Line>,
+}
+
+impl Cursor {
+    pub fn new(
+        origin: Vector2F,
+        block_width: f32,
+        line_height: f32,
+        color: Color,
+        shape: CursorShape,
+        block_text: Option<Line>,
+    ) -> Cursor {
+        Cursor {
+            origin,
+            block_width,
+            line_height,
+            color,
+            shape,
+            block_text,
+        }
+    }
+
+    pub fn bounding_rect(&self, origin: Vector2F) -> RectF {
+        RectF::new(
+            self.origin + origin,
+            vec2f(self.block_width, self.line_height),
+        )
+    }
+
+    pub fn paint(&self, origin: Vector2F, cx: &mut WindowContext) {
+        let bounds = match self.shape {
+            CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)),
+            CursorShape::Block | CursorShape::Hollow => RectF::new(
+                self.origin + origin,
+                vec2f(self.block_width, self.line_height),
+            ),
+            CursorShape::Underscore => RectF::new(
+                self.origin + origin + Vector2F::new(0.0, self.line_height - 2.0),
+                vec2f(self.block_width, 2.0),
+            ),
+        };
+
+        //Draw background or border quad
+        if matches!(self.shape, CursorShape::Hollow) {
+            cx.scene().push_quad(Quad {
+                bounds,
+                background: None,
+                border: Border::all(1., self.color).into(),
+                corner_radii: Default::default(),
+            });
+        } else {
+            cx.scene().push_quad(Quad {
+                bounds,
+                background: Some(self.color),
+                border: Default::default(),
+                corner_radii: Default::default(),
+            });
+        }
+
+        if let Some(block_text) = &self.block_text {
+            block_text.paint(self.origin + origin, bounds, self.line_height, cx);
+        }
+    }
+
+    pub fn shape(&self) -> CursorShape {
+        self.shape
+    }
+}
+
+#[derive(Debug)]
+pub struct HighlightedRange {
+    pub start_y: f32,
+    pub line_height: f32,
+    pub lines: Vec<HighlightedRangeLine>,
+    pub color: Color,
+    pub corner_radius: f32,
+}
+
+#[derive(Debug)]
+pub struct HighlightedRangeLine {
+    pub start_x: f32,
+    pub end_x: f32,
+}
+
+impl HighlightedRange {
+    pub fn paint(&self, bounds: RectF, cx: &mut WindowContext) {
+        if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
+            self.paint_lines(self.start_y, &self.lines[0..1], bounds, cx);
+            self.paint_lines(
+                self.start_y + self.line_height,
+                &self.lines[1..],
+                bounds,
+                cx,
+            );
+        } else {
+            self.paint_lines(self.start_y, &self.lines, bounds, cx);
+        }
+    }
+
+    fn paint_lines(
+        &self,
+        start_y: f32,
+        lines: &[HighlightedRangeLine],
+        bounds: RectF,
+        cx: &mut WindowContext,
+    ) {
+        if lines.is_empty() {
+            return;
+        }
+
+        let mut path = PathBuilder::new();
+        let first_line = lines.first().unwrap();
+        let last_line = lines.last().unwrap();
+
+        let first_top_left = vec2f(first_line.start_x, start_y);
+        let first_top_right = vec2f(first_line.end_x, start_y);
+
+        let curve_height = vec2f(0., self.corner_radius);
+        let curve_width = |start_x: f32, end_x: f32| {
+            let max = (end_x - start_x) / 2.;
+            let width = if max < self.corner_radius {
+                max
+            } else {
+                self.corner_radius
+            };
+
+            vec2f(width, 0.)
+        };
+
+        let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
+        path.reset(first_top_right - top_curve_width);
+        path.curve_to(first_top_right + curve_height, first_top_right);
+
+        let mut iter = lines.iter().enumerate().peekable();
+        while let Some((ix, line)) = iter.next() {
+            let bottom_right = vec2f(line.end_x, start_y + (ix + 1) as f32 * self.line_height);
+
+            if let Some((_, next_line)) = iter.peek() {
+                let next_top_right = vec2f(next_line.end_x, bottom_right.y());
+
+                match next_top_right.x().partial_cmp(&bottom_right.x()).unwrap() {
+                    Ordering::Equal => {
+                        path.line_to(bottom_right);
+                    }
+                    Ordering::Less => {
+                        let curve_width = curve_width(next_top_right.x(), bottom_right.x());
+                        path.line_to(bottom_right - curve_height);
+                        if self.corner_radius > 0. {
+                            path.curve_to(bottom_right - curve_width, bottom_right);
+                        }
+                        path.line_to(next_top_right + curve_width);
+                        if self.corner_radius > 0. {
+                            path.curve_to(next_top_right + curve_height, next_top_right);
+                        }
+                    }
+                    Ordering::Greater => {
+                        let curve_width = curve_width(bottom_right.x(), next_top_right.x());
+                        path.line_to(bottom_right - curve_height);
+                        if self.corner_radius > 0. {
+                            path.curve_to(bottom_right + curve_width, bottom_right);
+                        }
+                        path.line_to(next_top_right - curve_width);
+                        if self.corner_radius > 0. {
+                            path.curve_to(next_top_right + curve_height, next_top_right);
+                        }
+                    }
+                }
+            } else {
+                let curve_width = curve_width(line.start_x, line.end_x);
+                path.line_to(bottom_right - curve_height);
+                if self.corner_radius > 0. {
+                    path.curve_to(bottom_right - curve_width, bottom_right);
+                }
+
+                let bottom_left = vec2f(line.start_x, bottom_right.y());
+                path.line_to(bottom_left + curve_width);
+                if self.corner_radius > 0. {
+                    path.curve_to(bottom_left - curve_height, bottom_left);
+                }
+            }
+        }
+
+        if first_line.start_x > last_line.start_x {
+            let curve_width = curve_width(last_line.start_x, first_line.start_x);
+            let second_top_left = vec2f(last_line.start_x, start_y + self.line_height);
+            path.line_to(second_top_left + curve_height);
+            if self.corner_radius > 0. {
+                path.curve_to(second_top_left + curve_width, second_top_left);
+            }
+            let first_bottom_left = vec2f(first_line.start_x, second_top_left.y());
+            path.line_to(first_bottom_left - curve_width);
+            if self.corner_radius > 0. {
+                path.curve_to(first_bottom_left - curve_height, first_bottom_left);
+            }
+        }
+
+        path.line_to(first_top_left + curve_height);
+        if self.corner_radius > 0. {
+            path.curve_to(first_top_left + top_curve_width, first_top_left);
+        }
+        path.line_to(first_top_right - top_curve_width);
+
+        cx.scene().push_path(path.build(self.color, Some(bounds)));
+    }
+}
+
+fn range_to_bounds(
+    range: &Range<DisplayPoint>,
+    content_origin: Vector2F,
+    scroll_left: f32,
+    scroll_top: f32,
+    visible_row_range: &Range<u32>,
+    line_end_overshoot: f32,
+    position_map: &PositionMap,
+) -> impl Iterator<Item = RectF> {
+    let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new();
+
+    if range.start == range.end {
+        return bounds.into_iter();
+    }
+
+    let start_row = visible_row_range.start;
+    let end_row = visible_row_range.end;
+
+    let row_range = if range.end.column() == 0 {
+        cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
+    } else {
+        cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
+    };
+
+    let first_y =
+        content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
+
+    for (idx, row) in row_range.enumerate() {
+        let line_layout = &position_map.line_layouts[(row - start_row) as usize].line;
+
+        let start_x = if row == range.start.row() {
+            content_origin.x() + line_layout.x_for_index(range.start.column() as usize)
+                - scroll_left
+        } else {
+            content_origin.x() - scroll_left
+        };
+
+        let end_x = if row == range.end.row() {
+            content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left
+        } else {
+            content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left
+        };
+
+        bounds.push(RectF::from_points(
+            vec2f(start_x, first_y + position_map.line_height * idx as f32),
+            vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32),
+        ))
+    }
+
+    bounds.into_iter()
+}
+
+pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
+    delta.powf(1.5) / 100.0
+}
+
+fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 {
+    delta.powf(1.2) / 300.0
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::{BlockDisposition, BlockProperties},
+        editor_tests::{init_test, update_test_language_settings},
+        Editor, MultiBuffer,
+    };
+    use gpui::TestAppContext;
+    use language::language_settings;
+    use log::info;
+    use std::{num::NonZeroU32, sync::Arc};
+    use util::test::sample_text;
+
+    #[gpui::test]
+    fn test_layout_line_numbers(cx: &mut TestAppContext) {
+        init_test(cx, |_| {});
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
+        let element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+
+        let layouts = editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            element
+                .layout_line_numbers(
+                    0..6,
+                    &Default::default(),
+                    DisplayPoint::new(0, 0),
+                    false,
+                    &snapshot,
+                    cx,
+                )
+                .0
+        });
+        assert_eq!(layouts.len(), 6);
+
+        let relative_rows = editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3))
+        });
+        assert_eq!(relative_rows[&0], 3);
+        assert_eq!(relative_rows[&1], 2);
+        assert_eq!(relative_rows[&2], 1);
+        // current line has no relative number
+        assert_eq!(relative_rows[&4], 1);
+        assert_eq!(relative_rows[&5], 2);
+
+        // works if cursor is before screen
+        let relative_rows = editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+
+            element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1))
+        });
+        assert_eq!(relative_rows.len(), 3);
+        assert_eq!(relative_rows[&3], 2);
+        assert_eq!(relative_rows[&4], 3);
+        assert_eq!(relative_rows[&5], 4);
+
+        // works if cursor is after screen
+        let relative_rows = editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+
+            element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6))
+        });
+        assert_eq!(relative_rows.len(), 3);
+        assert_eq!(relative_rows[&0], 5);
+        assert_eq!(relative_rows[&1], 4);
+        assert_eq!(relative_rows[&2], 3);
+    }
+
+    #[gpui::test]
+    async fn test_vim_visual_selections(cx: &mut TestAppContext) {
+        init_test(cx, |_| {});
+
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
+        let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+        let (_, state) = editor.update(cx, |editor, cx| {
+            editor.cursor_shape = CursorShape::Block;
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([
+                    Point::new(0, 0)..Point::new(1, 0),
+                    Point::new(3, 2)..Point::new(3, 3),
+                    Point::new(5, 6)..Point::new(6, 0),
+                ]);
+            });
+            element.layout(
+                SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
+                editor,
+                cx,
+            )
+        });
+        assert_eq!(state.selections.len(), 1);
+        let local_selections = &state.selections[0].1;
+        assert_eq!(local_selections.len(), 3);
+        // moves cursor back one line
+        assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6));
+        assert_eq!(
+            local_selections[0].range,
+            DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0)
+        );
+
+        // moves cursor back one column
+        assert_eq!(
+            local_selections[1].range,
+            DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3)
+        );
+        assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2));
+
+        // leaves cursor on the max point
+        assert_eq!(
+            local_selections[2].range,
+            DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0)
+        );
+        assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0));
+
+        // active lines does not include 1 (even though the range of the selection does)
+        assert_eq!(
+            state.active_rows.keys().cloned().collect::<Vec<u32>>(),
+            vec![0, 3, 5, 6]
+        );
+
+        // multi-buffer support
+        // in DisplayPoint co-ordinates, this is what we're dealing with:
+        //  0: [[file
+        //  1:   header]]
+        //  2: aaaaaa
+        //  3: bbbbbb
+        //  4: cccccc
+        //  5:
+        //  6: ...
+        //  7: ffffff
+        //  8: gggggg
+        //  9: hhhhhh
+        // 10:
+        // 11: [[file
+        // 12:   header]]
+        // 13: bbbbbb
+        // 14: cccccc
+        // 15: dddddd
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_multi(
+                    [
+                        (
+                            &(sample_text(8, 6, 'a') + "\n"),
+                            vec![
+                                Point::new(0, 0)..Point::new(3, 0),
+                                Point::new(4, 0)..Point::new(7, 0),
+                            ],
+                        ),
+                        (
+                            &(sample_text(8, 6, 'a') + "\n"),
+                            vec![Point::new(1, 0)..Point::new(3, 0)],
+                        ),
+                    ],
+                    cx,
+                );
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
+        let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+        let (_, state) = editor.update(cx, |editor, cx| {
+            editor.cursor_shape = CursorShape::Block;
+            editor.change_selections(None, cx, |s| {
+                s.select_display_ranges([
+                    DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0),
+                    DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0),
+                ]);
+            });
+            element.layout(
+                SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
+                editor,
+                cx,
+            )
+        });
+
+        assert_eq!(state.selections.len(), 1);
+        let local_selections = &state.selections[0].1;
+        assert_eq!(local_selections.len(), 2);
+
+        // moves cursor on excerpt boundary back a line
+        // and doesn't allow selection to bleed through
+        assert_eq!(
+            local_selections[0].range,
+            DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0)
+        );
+        assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0));
+
+        // moves cursor on buffer boundary back two lines
+        // and doesn't allow selection to bleed through
+        assert_eq!(
+            local_selections[1].range,
+            DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0)
+        );
+        assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0));
+    }
+
+    #[gpui::test]
+    fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
+        init_test(cx, |_| {});
+
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple("", cx);
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
+
+        editor.update(cx, |editor, cx| {
+            editor.set_placeholder_text("hello", cx);
+            editor.insert_blocks(
+                [BlockProperties {
+                    style: BlockStyle::Fixed,
+                    disposition: BlockDisposition::Above,
+                    height: 3,
+                    position: Anchor::min(),
+                    render: Arc::new(|_| Empty::new().into_any()),
+                }],
+                None,
+                cx,
+            );
+
+            // Blur the editor so that it displays placeholder text.
+            cx.blur();
+        });
+
+        let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+        let (size, mut state) = editor.update(cx, |editor, cx| {
+            element.layout(
+                SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
+                editor,
+                cx,
+            )
+        });
+
+        assert_eq!(state.position_map.line_layouts.len(), 4);
+        assert_eq!(
+            state
+                .line_number_layouts
+                .iter()
+                .map(Option::is_some)
+                .collect::<Vec<_>>(),
+            &[false, false, false, true]
+        );
+
+        // Don't panic.
+        let bounds = RectF::new(Default::default(), size);
+        editor.update(cx, |editor, cx| {
+            element.paint(bounds, bounds, &mut state, editor, cx);
+        });
+    }
+
+    #[gpui::test]
+    fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
+        const TAB_SIZE: u32 = 4;
+
+        let input_text = "\t \t|\t| a b";
+        let expected_invisibles = vec![
+            Invisible::Tab {
+                line_start_offset: 0,
+            },
+            Invisible::Whitespace {
+                line_offset: TAB_SIZE as usize,
+            },
+            Invisible::Tab {
+                line_start_offset: TAB_SIZE as usize + 1,
+            },
+            Invisible::Tab {
+                line_start_offset: TAB_SIZE as usize * 2 + 1,
+            },
+            Invisible::Whitespace {
+                line_offset: TAB_SIZE as usize * 3 + 1,
+            },
+            Invisible::Whitespace {
+                line_offset: TAB_SIZE as usize * 3 + 3,
+            },
+        ];
+        assert_eq!(
+            expected_invisibles.len(),
+            input_text
+                .chars()
+                .filter(|initial_char| initial_char.is_whitespace())
+                .count(),
+            "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
+        );
+
+        init_test(cx, |s| {
+            s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+            s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
+        });
+
+        let actual_invisibles =
+            collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0);
+
+        assert_eq!(expected_invisibles, actual_invisibles);
+    }
+
+    #[gpui::test]
+    fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
+        init_test(cx, |s| {
+            s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+            s.defaults.tab_size = NonZeroU32::new(4);
+        });
+
+        for editor_mode_without_invisibles in [
+            EditorMode::SingleLine,
+            EditorMode::AutoHeight { max_lines: 100 },
+        ] {
+            let invisibles = collect_invisibles_from_new_editor(
+                cx,
+                editor_mode_without_invisibles,
+                "\t\t\t| | a b",
+                500.0,
+            );
+            assert!(invisibles.is_empty(),
+                "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
+        }
+    }
+
+    #[gpui::test]
+    fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
+        let tab_size = 4;
+        let input_text = "a\tbcd   ".repeat(9);
+        let repeated_invisibles = [
+            Invisible::Tab {
+                line_start_offset: 1,
+            },
+            Invisible::Whitespace {
+                line_offset: tab_size as usize + 3,
+            },
+            Invisible::Whitespace {
+                line_offset: tab_size as usize + 4,
+            },
+            Invisible::Whitespace {
+                line_offset: tab_size as usize + 5,
+            },
+        ];
+        let expected_invisibles = std::iter::once(repeated_invisibles)
+            .cycle()
+            .take(9)
+            .flatten()
+            .collect::<Vec<_>>();
+        assert_eq!(
+            expected_invisibles.len(),
+            input_text
+                .chars()
+                .filter(|initial_char| initial_char.is_whitespace())
+                .count(),
+            "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
+        );
+        info!("Expected invisibles: {expected_invisibles:?}");
+
+        init_test(cx, |_| {});
+
+        // Put the same string with repeating whitespace pattern into editors of various size,
+        // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
+        let resize_step = 10.0;
+        let mut editor_width = 200.0;
+        while editor_width <= 1000.0 {
+            update_test_language_settings(cx, |s| {
+                s.defaults.tab_size = NonZeroU32::new(tab_size);
+                s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+                s.defaults.preferred_line_length = Some(editor_width as u32);
+                s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
+            });
+
+            let actual_invisibles =
+                collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, editor_width);
+
+            // Whatever the editor size is, ensure it has the same invisible kinds in the same order
+            // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
+            let mut i = 0;
+            for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
+                i = actual_index;
+                match expected_invisibles.get(i) {
+                    Some(expected_invisible) => match (expected_invisible, actual_invisible) {
+                        (Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
+                        | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
+                        _ => {
+                            panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
+                        }
+                    },
+                    None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"),
+                }
+            }
+            let missing_expected_invisibles = &expected_invisibles[i + 1..];
+            assert!(
+                missing_expected_invisibles.is_empty(),
+                "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
+            );
+
+            editor_width += resize_step;
+        }
+    }
+
+    fn collect_invisibles_from_new_editor(
+        cx: &mut TestAppContext,
+        editor_mode: EditorMode,
+        input_text: &str,
+        editor_width: f32,
+    ) -> Vec<Invisible> {
+        info!(
+            "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'"
+        );
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple(&input_text, cx);
+                Editor::new(editor_mode, buffer, None, None, cx)
+            })
+            .root(cx);
+
+        let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+        let (_, layout_state) = editor.update(cx, |editor, cx| {
+            editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
+            editor.set_wrap_width(Some(editor_width), cx);
+
+            element.layout(
+                SizeConstraint::new(vec2f(editor_width, 500.), vec2f(editor_width, 500.)),
+                editor,
+                cx,
+            )
+        });
+
+        layout_state
+            .position_map
+            .line_layouts
+            .iter()
+            .map(|line_with_invisibles| &line_with_invisibles.invisibles)
+            .flatten()
+            .cloned()
+            .collect()
+    }
+}

crates/editor2/src/git.rs ๐Ÿ”—

@@ -0,0 +1,282 @@
+use std::ops::Range;
+
+use git::diff::{DiffHunk, DiffHunkStatus};
+use language::Point;
+
+use crate::{
+    display_map::{DisplaySnapshot, ToDisplayPoint},
+    AnchorRangeExt,
+};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum DisplayDiffHunk {
+    Folded {
+        display_row: u32,
+    },
+
+    Unfolded {
+        display_row_range: Range<u32>,
+        status: DiffHunkStatus,
+    },
+}
+
+impl DisplayDiffHunk {
+    pub fn start_display_row(&self) -> u32 {
+        match self {
+            &DisplayDiffHunk::Folded { display_row } => display_row,
+            DisplayDiffHunk::Unfolded {
+                display_row_range, ..
+            } => display_row_range.start,
+        }
+    }
+
+    pub fn contains_display_row(&self, display_row: u32) -> bool {
+        let range = match self {
+            &DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
+
+            DisplayDiffHunk::Unfolded {
+                display_row_range, ..
+            } => display_row_range.start..=display_row_range.end,
+        };
+
+        range.contains(&display_row)
+    }
+}
+
+pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) -> DisplayDiffHunk {
+    let hunk_start_point = Point::new(hunk.buffer_range.start, 0);
+    let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
+    let hunk_end_point_sub = Point::new(
+        hunk.buffer_range
+            .end
+            .saturating_sub(1)
+            .max(hunk.buffer_range.start),
+        0,
+    );
+
+    let is_removal = hunk.status() == DiffHunkStatus::Removed;
+
+    let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(2), 0);
+    let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
+    let folds_range = folds_start..folds_end;
+
+    let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
+        let fold_point_range = fold_range.to_point(&snapshot.buffer_snapshot);
+        let fold_point_range = fold_point_range.start..=fold_point_range.end;
+
+        let folded_start = fold_point_range.contains(&hunk_start_point);
+        let folded_end = fold_point_range.contains(&hunk_end_point_sub);
+        let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
+
+        (folded_start && folded_end) || (is_removal && folded_start_sub)
+    });
+
+    if let Some(fold) = containing_fold {
+        let row = fold.start.to_display_point(snapshot).row();
+        DisplayDiffHunk::Folded { display_row: row }
+    } else {
+        let start = hunk_start_point.to_display_point(snapshot).row();
+
+        let hunk_end_row = hunk.buffer_range.end.max(hunk.buffer_range.start);
+        let hunk_end_point = Point::new(hunk_end_row, 0);
+        let end = hunk_end_point.to_display_point(snapshot).row();
+
+        DisplayDiffHunk::Unfolded {
+            display_row_range: start..end,
+            status: hunk.status(),
+        }
+    }
+}
+
+#[cfg(any(test, feature = "test_support"))]
+mod tests {
+    use crate::editor_tests::init_test;
+    use crate::Point;
+    use gpui::TestAppContext;
+    use multi_buffer::{ExcerptRange, MultiBuffer};
+    use project::{FakeFs, Project};
+    use unindent::Unindent;
+    #[gpui::test]
+    async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
+        use git::diff::DiffHunkStatus;
+        init_test(cx, |_| {});
+
+        let fs = FakeFs::new(cx.background());
+        let project = Project::test(fs, [], cx).await;
+
+        // buffer has two modified hunks with two rows each
+        let buffer_1 = project
+            .update(cx, |project, cx| {
+                project.create_buffer(
+                    "
+                        1.zero
+                        1.ONE
+                        1.TWO
+                        1.three
+                        1.FOUR
+                        1.FIVE
+                        1.six
+                    "
+                    .unindent()
+                    .as_str(),
+                    None,
+                    cx,
+                )
+            })
+            .unwrap();
+        buffer_1.update(cx, |buffer, cx| {
+            buffer.set_diff_base(
+                Some(
+                    "
+                        1.zero
+                        1.one
+                        1.two
+                        1.three
+                        1.four
+                        1.five
+                        1.six
+                    "
+                    .unindent(),
+                ),
+                cx,
+            );
+        });
+
+        // buffer has a deletion hunk and an insertion hunk
+        let buffer_2 = project
+            .update(cx, |project, cx| {
+                project.create_buffer(
+                    "
+                        2.zero
+                        2.one
+                        2.two
+                        2.three
+                        2.four
+                        2.five
+                        2.six
+                    "
+                    .unindent()
+                    .as_str(),
+                    None,
+                    cx,
+                )
+            })
+            .unwrap();
+        buffer_2.update(cx, |buffer, cx| {
+            buffer.set_diff_base(
+                Some(
+                    "
+                        2.zero
+                        2.one
+                        2.one-and-a-half
+                        2.two
+                        2.three
+                        2.four
+                        2.six
+                    "
+                    .unindent(),
+                ),
+                cx,
+            );
+        });
+
+        cx.foreground().run_until_parked();
+
+        let multibuffer = cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    // excerpt ends in the middle of a modified hunk
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt begins in the middle of a modified hunk
+                    ExcerptRange {
+                        context: Point::new(5, 0)..Point::new(6, 5),
+                        primary: Default::default(),
+                    },
+                ],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [
+                    // excerpt ends at a deletion
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt starts at a deletion
+                    ExcerptRange {
+                        context: Point::new(2, 0)..Point::new(2, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt fully contains a deletion hunk
+                    ExcerptRange {
+                        context: Point::new(1, 0)..Point::new(2, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt fully contains an insertion hunk
+                    ExcerptRange {
+                        context: Point::new(4, 0)..Point::new(6, 5),
+                        primary: Default::default(),
+                    },
+                ],
+                cx,
+            );
+            multibuffer
+        });
+
+        let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
+
+        assert_eq!(
+            snapshot.text(),
+            "
+                1.zero
+                1.ONE
+                1.FIVE
+                1.six
+                2.zero
+                2.one
+                2.two
+                2.one
+                2.two
+                2.four
+                2.five
+                2.six"
+                .unindent()
+        );
+
+        let expected = [
+            (DiffHunkStatus::Modified, 1..2),
+            (DiffHunkStatus::Modified, 2..3),
+            //TODO: Define better when and where removed hunks show up at range extremities
+            (DiffHunkStatus::Removed, 6..6),
+            (DiffHunkStatus::Removed, 8..8),
+            (DiffHunkStatus::Added, 10..11),
+        ];
+
+        assert_eq!(
+            snapshot
+                .git_diff_hunks_in_range(0..12)
+                .map(|hunk| (hunk.status(), hunk.buffer_range))
+                .collect::<Vec<_>>(),
+            &expected,
+        );
+
+        assert_eq!(
+            snapshot
+                .git_diff_hunks_in_range_rev(0..12)
+                .map(|hunk| (hunk.status(), hunk.buffer_range))
+                .collect::<Vec<_>>(),
+            expected
+                .iter()
+                .rev()
+                .cloned()
+                .collect::<Vec<_>>()
+                .as_slice(),
+        );
+    }
+}

crates/editor2/src/highlight_matching_bracket.rs ๐Ÿ”—

@@ -0,0 +1,138 @@
+use gpui::ViewContext;
+
+use crate::{Editor, RangeToAnchorExt};
+
+enum MatchingBracketHighlight {}
+
+pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
+    editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
+
+    let newest_selection = editor.selections.newest::<usize>(cx);
+    // Don't highlight brackets if the selection isn't empty
+    if !newest_selection.is_empty() {
+        return;
+    }
+
+    let head = newest_selection.head();
+    let snapshot = editor.snapshot(cx);
+    if let Some((opening_range, closing_range)) = snapshot
+        .buffer_snapshot
+        .innermost_enclosing_bracket_ranges(head..head)
+    {
+        editor.highlight_background::<MatchingBracketHighlight>(
+            vec![
+                opening_range.to_anchors(&snapshot.buffer_snapshot),
+                closing_range.to_anchors(&snapshot.buffer_snapshot),
+            ],
+            |theme| theme.editor.document_highlight_read_background,
+            cx,
+        )
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
+    use indoc::indoc;
+    use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
+
+    #[gpui::test]
+    async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Rust".into(),
+                    path_suffixes: vec!["rs".to_string()],
+                    brackets: BracketPairConfig {
+                        pairs: vec![
+                            BracketPair {
+                                start: "{".to_string(),
+                                end: "}".to_string(),
+                                close: false,
+                                newline: true,
+                            },
+                            BracketPair {
+                                start: "(".to_string(),
+                                end: ")".to_string(),
+                                close: false,
+                                newline: true,
+                            },
+                        ],
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )
+            .with_brackets_query(indoc! {r#"
+                ("{" @open "}" @close)
+                ("(" @open ")" @close)
+                "#})
+            .unwrap(),
+            Default::default(),
+            cx,
+        )
+        .await;
+
+        // positioning cursor inside bracket highlights both
+        cx.set_state(indoc! {r#"
+            pub fn test("Test ห‡argument") {
+                another_test(1, 2, 3);
+            }
+        "#});
+        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn testยซ(ยป"Test argument"ยซ)ยป {
+                another_test(1, 2, 3);
+            }
+        "#});
+
+        cx.set_state(indoc! {r#"
+            pub fn test("Test argument") {
+                another_test(1, ห‡2, 3);
+            }
+        "#});
+        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn test("Test argument") {
+                another_testยซ(ยป1, 2, 3ยซ)ยป;
+            }
+        "#});
+
+        cx.set_state(indoc! {r#"
+            pub fn test("Test argument") {
+                anotherห‡_test(1, 2, 3);
+            }
+        "#});
+        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn test("Test argument") ยซ{ยป
+                another_test(1, 2, 3);
+            ยซ}ยป
+        "#});
+
+        // positioning outside of brackets removes highlight
+        cx.set_state(indoc! {r#"
+            pub fห‡n test("Test argument") {
+                another_test(1, 2, 3);
+            }
+        "#});
+        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn test("Test argument") {
+                another_test(1, 2, 3);
+            }
+        "#});
+
+        // non empty selection dismisses highlight
+        cx.set_state(indoc! {r#"
+            pub fn test("Teยซst argห‡ยปument") {
+                another_test(1, 2, 3);
+            }
+        "#});
+        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn test("Test argument") {
+                another_test(1, 2, 3);
+            }
+        "#});
+    }
+}

crates/editor2/src/hover_popover.rs ๐Ÿ”—

@@ -0,0 +1,1329 @@
+use crate::{
+    display_map::{InlayOffset, ToDisplayPoint},
+    link_go_to_definition::{InlayHighlight, RangeInEditor},
+    Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
+    ExcerptId, RangeToAnchorExt,
+};
+use futures::FutureExt;
+use gpui::{
+    actions,
+    elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
+    platform::{CursorStyle, MouseButton},
+    AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle,
+};
+use language::{
+    markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown,
+};
+use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
+use std::{ops::Range, sync::Arc, time::Duration};
+use util::TryFutureExt;
+use workspace::Workspace;
+
+pub const HOVER_DELAY_MILLIS: u64 = 350;
+pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
+
+pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
+pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
+pub const HOVER_POPOVER_GAP: f32 = 10.;
+
+actions!(editor, [Hover]);
+
+pub fn init(cx: &mut AppContext) {
+    cx.add_action(hover);
+}
+
+/// Bindable action which uses the most recent selection head to trigger a hover
+pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
+    let head = editor.selections.newest_display(cx).head();
+    show_hover(editor, head, true, cx);
+}
+
+/// The internal hover action dispatches between `show_hover` or `hide_hover`
+/// depending on whether a point to hover over is provided.
+pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
+    if settings::get::<EditorSettings>(cx).hover_popover_enabled {
+        if let Some(point) = point {
+            show_hover(editor, point, false, cx);
+        } else {
+            hide_hover(editor, cx);
+        }
+    }
+}
+
+pub struct InlayHover {
+    pub excerpt: ExcerptId,
+    pub range: InlayHighlight,
+    pub tooltip: HoverBlock,
+}
+
+pub fn find_hovered_hint_part(
+    label_parts: Vec<InlayHintLabelPart>,
+    hint_start: InlayOffset,
+    hovered_offset: InlayOffset,
+) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
+    if hovered_offset >= hint_start {
+        let mut hovered_character = (hovered_offset - hint_start).0;
+        let mut part_start = hint_start;
+        for part in label_parts {
+            let part_len = part.value.chars().count();
+            if hovered_character > part_len {
+                hovered_character -= part_len;
+                part_start.0 += part_len;
+            } else {
+                let part_end = InlayOffset(part_start.0 + part_len);
+                return Some((part, part_start..part_end));
+            }
+        }
+    }
+    None
+}
+
+pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
+    if settings::get::<EditorSettings>(cx).hover_popover_enabled {
+        if editor.pending_rename.is_some() {
+            return;
+        }
+
+        let Some(project) = editor.project.clone() else {
+            return;
+        };
+
+        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
+            if let RangeInEditor::Inlay(range) = symbol_range {
+                if range == &inlay_hover.range {
+                    // Hover triggered from same location as last time. Don't show again.
+                    return;
+                }
+            }
+            hide_hover(editor, cx);
+        }
+
+        let task = cx.spawn(|this, mut cx| {
+            async move {
+                cx.background()
+                    .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
+                    .await;
+                this.update(&mut cx, |this, _| {
+                    this.hover_state.diagnostic_popover = None;
+                })?;
+
+                let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
+                let blocks = vec![inlay_hover.tooltip];
+                let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
+
+                let hover_popover = InfoPopover {
+                    project: project.clone(),
+                    symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
+                    blocks,
+                    parsed_content,
+                };
+
+                this.update(&mut cx, |this, cx| {
+                    // Highlight the selected symbol using a background highlight
+                    this.highlight_inlay_background::<HoverState>(
+                        vec![inlay_hover.range],
+                        |theme| theme.editor.hover_popover.highlight,
+                        cx,
+                    );
+                    this.hover_state.info_popover = Some(hover_popover);
+                    cx.notify();
+                })?;
+
+                anyhow::Ok(())
+            }
+            .log_err()
+        });
+
+        editor.hover_state.info_task = Some(task);
+    }
+}
+
+/// Hides the type information popup.
+/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
+/// selections changed.
+pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
+    let did_hide = editor.hover_state.info_popover.take().is_some()
+        | editor.hover_state.diagnostic_popover.take().is_some();
+
+    editor.hover_state.info_task = None;
+    editor.hover_state.triggered_from = None;
+
+    editor.clear_background_highlights::<HoverState>(cx);
+
+    if did_hide {
+        cx.notify();
+    }
+
+    did_hide
+}
+
+/// Queries the LSP and shows type info and documentation
+/// about the symbol the mouse is currently hovering over.
+/// Triggered by the `Hover` action when the cursor may be over a symbol.
+fn show_hover(
+    editor: &mut Editor,
+    point: DisplayPoint,
+    ignore_timeout: bool,
+    cx: &mut ViewContext<Editor>,
+) {
+    if editor.pending_rename.is_some() {
+        return;
+    }
+
+    let snapshot = editor.snapshot(cx);
+    let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
+
+    let (buffer, buffer_position) = if let Some(output) = editor
+        .buffer
+        .read(cx)
+        .text_anchor_for_position(multibuffer_offset, cx)
+    {
+        output
+    } else {
+        return;
+    };
+
+    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
+        .buffer()
+        .read(cx)
+        .excerpt_containing(multibuffer_offset, cx)
+    {
+        excerpt_id
+    } else {
+        return;
+    };
+
+    let project = if let Some(project) = editor.project.clone() {
+        project
+    } else {
+        return;
+    };
+
+    if !ignore_timeout {
+        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
+            if symbol_range
+                .as_text_range()
+                .map(|range| {
+                    range
+                        .to_offset(&snapshot.buffer_snapshot)
+                        .contains(&multibuffer_offset)
+                })
+                .unwrap_or(false)
+            {
+                // Hover triggered from same location as last time. Don't show again.
+                return;
+            } else {
+                hide_hover(editor, cx);
+            }
+        }
+    }
+
+    // Get input anchor
+    let anchor = snapshot
+        .buffer_snapshot
+        .anchor_at(multibuffer_offset, Bias::Left);
+
+    // Don't request again if the location is the same as the previous request
+    if let Some(triggered_from) = &editor.hover_state.triggered_from {
+        if triggered_from
+            .cmp(&anchor, &snapshot.buffer_snapshot)
+            .is_eq()
+        {
+            return;
+        }
+    }
+
+    let task = cx.spawn(|this, mut cx| {
+        async move {
+            // If we need to delay, delay a set amount initially before making the lsp request
+            let delay = if !ignore_timeout {
+                // Construct delay task to wait for later
+                let total_delay = Some(
+                    cx.background()
+                        .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
+                );
+
+                cx.background()
+                    .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
+                    .await;
+                total_delay
+            } else {
+                None
+            };
+
+            // query the LSP for hover info
+            let hover_request = cx.update(|cx| {
+                project.update(cx, |project, cx| {
+                    project.hover(&buffer, buffer_position, cx)
+                })
+            });
+
+            if let Some(delay) = delay {
+                delay.await;
+            }
+
+            // If there's a diagnostic, assign it on the hover state and notify
+            let local_diagnostic = snapshot
+                .buffer_snapshot
+                .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
+                // Find the entry with the most specific range
+                .min_by_key(|entry| entry.range.end - entry.range.start)
+                .map(|entry| DiagnosticEntry {
+                    diagnostic: entry.diagnostic,
+                    range: entry.range.to_anchors(&snapshot.buffer_snapshot),
+                });
+
+            // Pull the primary diagnostic out so we can jump to it if the popover is clicked
+            let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
+                snapshot
+                    .buffer_snapshot
+                    .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
+                    .find(|diagnostic| diagnostic.diagnostic.is_primary)
+                    .map(|entry| DiagnosticEntry {
+                        diagnostic: entry.diagnostic,
+                        range: entry.range.to_anchors(&snapshot.buffer_snapshot),
+                    })
+            });
+
+            this.update(&mut cx, |this, _| {
+                this.hover_state.diagnostic_popover =
+                    local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
+                        local_diagnostic,
+                        primary_diagnostic,
+                    });
+            })?;
+
+            let hover_result = hover_request.await.ok().flatten();
+            let hover_popover = match hover_result {
+                Some(hover_result) if !hover_result.is_empty() => {
+                    // Create symbol range of anchors for highlighting and filtering of future requests.
+                    let range = if let Some(range) = hover_result.range {
+                        let start = snapshot
+                            .buffer_snapshot
+                            .anchor_in_excerpt(excerpt_id.clone(), range.start);
+                        let end = snapshot
+                            .buffer_snapshot
+                            .anchor_in_excerpt(excerpt_id.clone(), range.end);
+
+                        start..end
+                    } else {
+                        anchor..anchor
+                    };
+
+                    let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
+                    let blocks = hover_result.contents;
+                    let language = hover_result.language;
+                    let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
+
+                    Some(InfoPopover {
+                        project: project.clone(),
+                        symbol_range: RangeInEditor::Text(range),
+                        blocks,
+                        parsed_content,
+                    })
+                }
+
+                _ => None,
+            };
+
+            this.update(&mut cx, |this, cx| {
+                if let Some(symbol_range) = hover_popover
+                    .as_ref()
+                    .and_then(|hover_popover| hover_popover.symbol_range.as_text_range())
+                {
+                    // Highlight the selected symbol using a background highlight
+                    this.highlight_background::<HoverState>(
+                        vec![symbol_range],
+                        |theme| theme.editor.hover_popover.highlight,
+                        cx,
+                    );
+                } else {
+                    this.clear_background_highlights::<HoverState>(cx);
+                }
+
+                this.hover_state.info_popover = hover_popover;
+                cx.notify();
+            })?;
+
+            Ok::<_, anyhow::Error>(())
+        }
+        .log_err()
+    });
+
+    editor.hover_state.info_task = Some(task);
+}
+
+async fn parse_blocks(
+    blocks: &[HoverBlock],
+    language_registry: &Arc<LanguageRegistry>,
+    language: Option<Arc<Language>>,
+) -> markdown::ParsedMarkdown {
+    let mut text = String::new();
+    let mut highlights = Vec::new();
+    let mut region_ranges = Vec::new();
+    let mut regions = Vec::new();
+
+    for block in blocks {
+        match &block.kind {
+            HoverBlockKind::PlainText => {
+                markdown::new_paragraph(&mut text, &mut Vec::new());
+                text.push_str(&block.text);
+            }
+
+            HoverBlockKind::Markdown => {
+                markdown::parse_markdown_block(
+                    &block.text,
+                    language_registry,
+                    language.clone(),
+                    &mut text,
+                    &mut highlights,
+                    &mut region_ranges,
+                    &mut regions,
+                )
+                .await
+            }
+
+            HoverBlockKind::Code { language } => {
+                if let Some(language) = language_registry
+                    .language_for_name(language)
+                    .now_or_never()
+                    .and_then(Result::ok)
+                {
+                    markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
+                } else {
+                    text.push_str(&block.text);
+                }
+            }
+        }
+    }
+
+    ParsedMarkdown {
+        text: text.trim().to_string(),
+        highlights,
+        region_ranges,
+        regions,
+    }
+}
+
+#[derive(Default)]
+pub struct HoverState {
+    pub info_popover: Option<InfoPopover>,
+    pub diagnostic_popover: Option<DiagnosticPopover>,
+    pub triggered_from: Option<Anchor>,
+    pub info_task: Option<Task<Option<()>>>,
+}
+
+impl HoverState {
+    pub fn visible(&self) -> bool {
+        self.info_popover.is_some() || self.diagnostic_popover.is_some()
+    }
+
+    pub fn render(
+        &mut self,
+        snapshot: &EditorSnapshot,
+        style: &EditorStyle,
+        visible_rows: Range<u32>,
+        workspace: Option<WeakViewHandle<Workspace>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
+        // If there is a diagnostic, position the popovers based on that.
+        // Otherwise use the start of the hover range
+        let anchor = self
+            .diagnostic_popover
+            .as_ref()
+            .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
+            .or_else(|| {
+                self.info_popover
+                    .as_ref()
+                    .map(|info_popover| match &info_popover.symbol_range {
+                        RangeInEditor::Text(range) => &range.start,
+                        RangeInEditor::Inlay(range) => &range.inlay_position,
+                    })
+            })?;
+        let point = anchor.to_display_point(&snapshot.display_snapshot);
+
+        // Don't render if the relevant point isn't on screen
+        if !self.visible() || !visible_rows.contains(&point.row()) {
+            return None;
+        }
+
+        let mut elements = Vec::new();
+
+        if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
+            elements.push(diagnostic_popover.render(style, cx));
+        }
+        if let Some(info_popover) = self.info_popover.as_mut() {
+            elements.push(info_popover.render(style, workspace, cx));
+        }
+
+        Some((point, elements))
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct InfoPopover {
+    pub project: ModelHandle<Project>,
+    symbol_range: RangeInEditor,
+    pub blocks: Vec<HoverBlock>,
+    parsed_content: ParsedMarkdown,
+}
+
+impl InfoPopover {
+    pub fn render(
+        &mut self,
+        style: &EditorStyle,
+        workspace: Option<WeakViewHandle<Workspace>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> AnyElement<Editor> {
+        MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
+            Flex::column()
+                .scrollable::<HoverBlock>(0, None, cx)
+                .with_child(crate::render_parsed_markdown::<HoverBlock>(
+                    &self.parsed_content,
+                    style,
+                    workspace,
+                    cx,
+                ))
+                .contained()
+                .with_style(style.hover_popover.container)
+        })
+        .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
+        .with_cursor_style(CursorStyle::Arrow)
+        .with_padding(Padding {
+            bottom: HOVER_POPOVER_GAP,
+            top: HOVER_POPOVER_GAP,
+            ..Default::default()
+        })
+        .into_any()
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct DiagnosticPopover {
+    local_diagnostic: DiagnosticEntry<Anchor>,
+    primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
+}
+
+impl DiagnosticPopover {
+    pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
+        enum PrimaryDiagnostic {}
+
+        let mut text_style = style.hover_popover.prose.clone();
+        text_style.font_size = style.text.font_size;
+        let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone();
+
+        let text = match &self.local_diagnostic.diagnostic.source {
+            Some(source) => Text::new(
+                format!("{source}: {}", self.local_diagnostic.diagnostic.message),
+                text_style,
+            )
+            .with_highlights(vec![(0..source.len(), diagnostic_source_style)]),
+
+            None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style),
+        };
+
+        let container_style = match self.local_diagnostic.diagnostic.severity {
+            DiagnosticSeverity::HINT => style.hover_popover.info_container,
+            DiagnosticSeverity::INFORMATION => style.hover_popover.info_container,
+            DiagnosticSeverity::WARNING => style.hover_popover.warning_container,
+            DiagnosticSeverity::ERROR => style.hover_popover.error_container,
+            _ => style.hover_popover.container,
+        };
+
+        let tooltip_style = theme::current(cx).tooltip.clone();
+
+        MouseEventHandler::new::<DiagnosticPopover, _>(0, cx, |_, _| {
+            text.with_soft_wrap(true)
+                .contained()
+                .with_style(container_style)
+        })
+        .with_padding(Padding {
+            top: HOVER_POPOVER_GAP,
+            bottom: HOVER_POPOVER_GAP,
+            ..Default::default()
+        })
+        .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
+        .on_click(MouseButton::Left, |_, this, cx| {
+            this.go_to_diagnostic(&Default::default(), cx)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .with_tooltip::<PrimaryDiagnostic>(
+            0,
+            "Go To Diagnostic".to_string(),
+            Some(Box::new(crate::GoToDiagnostic)),
+            tooltip_style,
+            cx,
+        )
+        .into_any()
+    }
+
+    pub fn activation_info(&self) -> (usize, Anchor) {
+        let entry = self
+            .primary_diagnostic
+            .as_ref()
+            .unwrap_or(&self.local_diagnostic);
+
+        (entry.diagnostic.group_id, entry.range.start.clone())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        editor_tests::init_test,
+        element::PointForPosition,
+        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+        link_go_to_definition::update_inlay_link_and_hover_points,
+        test::editor_lsp_test_context::EditorLspTestContext,
+        InlayId,
+    };
+    use collections::BTreeSet;
+    use gpui::fonts::{HighlightStyle, Underline, Weight};
+    use indoc::indoc;
+    use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
+    use lsp::LanguageServerId;
+    use project::{HoverBlock, HoverBlockKind};
+    use smol::stream::StreamExt;
+    use unindent::Unindent;
+    use util::test::marked_text_ranges;
+
+    #[gpui::test]
+    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Basic hover delays and then pops without moving the mouse
+        cx.set_state(indoc! {"
+            fn ห‡test() { println!(); }
+        "});
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { printห‡ln!(); }
+        "});
+
+        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
+        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
+
+        // After delay, hover should be visible.
+        let symbol_range = cx.lsp_range(indoc! {"
+            fn test() { ยซprintln!ยป(); }
+        "});
+        let mut requests =
+            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+                Ok(Some(lsp::Hover {
+                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                        kind: lsp::MarkupKind::Markdown,
+                        value: "some basic docs".to_string(),
+                    }),
+                    range: Some(symbol_range),
+                }))
+            });
+        cx.foreground()
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        requests.next().await;
+
+        cx.editor(|editor, _| {
+            assert!(editor.hover_state.visible());
+            assert_eq!(
+                editor.hover_state.info_popover.clone().unwrap().blocks,
+                vec![HoverBlock {
+                    text: "some basic docs".to_string(),
+                    kind: HoverBlockKind::Markdown,
+                },]
+            )
+        });
+
+        // Mouse moved with no hover response dismisses
+        let hover_point = cx.display_point(indoc! {"
+            fn teห‡st() { println!(); }
+        "});
+        let mut request = cx
+            .lsp
+            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
+        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
+        cx.foreground()
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        request.next().await;
+        cx.editor(|editor, _| {
+            assert!(!editor.hover_state.visible());
+        });
+    }
+
+    #[gpui::test]
+    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fห‡n test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            ยซfnยป test() { println!(); }
+        "});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                    kind: lsp::MarkupKind::Markdown,
+                    value: "some other basic docs".to_string(),
+                }),
+                range: Some(symbol_range),
+            }))
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, _| {
+            assert_eq!(
+                editor.hover_state.info_popover.clone().unwrap().blocks,
+                vec![HoverBlock {
+                    text: "some other basic docs".to_string(),
+                    kind: HoverBlockKind::Markdown,
+                }]
+            )
+        });
+    }
+
+    #[gpui::test]
+    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fห‡n test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            ยซfnยป test() { println!(); }
+        "});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Array(vec![
+                    lsp::MarkedString::String("regular text for hover to show".to_string()),
+                    lsp::MarkedString::String("".to_string()),
+                    lsp::MarkedString::LanguageString(lsp::LanguageString {
+                        language: "Rust".to_string(),
+                        value: "".to_string(),
+                    }),
+                ]),
+                range: Some(symbol_range),
+            }))
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, _| {
+            assert_eq!(
+                editor.hover_state.info_popover.clone().unwrap().blocks,
+                vec![HoverBlock {
+                    text: "regular text for hover to show".to_string(),
+                    kind: HoverBlockKind::Markdown,
+                }],
+                "No empty string hovers should be shown"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fห‡n test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            ยซfnยป test() { println!(); }
+        "});
+
+        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
+        let markdown_string = format!("\n```rust\n{code_str}```");
+
+        let closure_markdown_string = markdown_string.clone();
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
+            let future_markdown_string = closure_markdown_string.clone();
+            async move {
+                Ok(Some(lsp::Hover {
+                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                        kind: lsp::MarkupKind::Markdown,
+                        value: future_markdown_string,
+                    }),
+                    range: Some(symbol_range),
+                }))
+            }
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, _| {
+            let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
+            assert_eq!(
+                blocks,
+                vec![HoverBlock {
+                    text: markdown_string,
+                    kind: HoverBlockKind::Markdown,
+                }],
+            );
+
+            let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
+            assert_eq!(
+                rendered.text,
+                code_str.trim(),
+                "Should not have extra line breaks at end of rendered hover"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
+        // info popover once request completes
+        cx.set_state(indoc! {"
+            fn teห‡st() { println!(); }
+        "});
+
+        // Send diagnostic to client
+        let range = cx.text_anchor_range(indoc! {"
+            fn ยซtestยป() { println!(); }
+        "});
+        cx.update_buffer(|buffer, cx| {
+            let snapshot = buffer.text_snapshot();
+            let set = DiagnosticSet::from_sorted_entries(
+                vec![DiagnosticEntry {
+                    range,
+                    diagnostic: Diagnostic {
+                        message: "A test diagnostic message.".to_string(),
+                        ..Default::default()
+                    },
+                }],
+                &snapshot,
+            );
+            buffer.update_diagnostics(LanguageServerId(0), set, cx);
+        });
+
+        // Hover pops diagnostic immediately
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        cx.foreground().run_until_parked();
+
+        cx.editor(|Editor { hover_state, .. }, _| {
+            assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
+        });
+
+        // Info Popover shows after request responded to
+        let range = cx.lsp_range(indoc! {"
+            fn ยซtestยป() { println!(); }
+        "});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                    kind: lsp::MarkupKind::Markdown,
+                    value: "some new docs".to_string(),
+                }),
+                range: Some(range),
+            }))
+        });
+        cx.foreground()
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+
+        cx.foreground().run_until_parked();
+        cx.editor(|Editor { hover_state, .. }, _| {
+            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
+        });
+    }
+
+    #[gpui::test]
+    fn test_render_blocks(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        cx.add_window(|cx| {
+            let editor = Editor::single_line(None, cx);
+            let style = editor.style(cx);
+
+            struct Row {
+                blocks: Vec<HoverBlock>,
+                expected_marked_text: String,
+                expected_styles: Vec<HighlightStyle>,
+            }
+
+            let rows = &[
+                // Strong emphasis
+                Row {
+                    blocks: vec![HoverBlock {
+                        text: "one **two** three".to_string(),
+                        kind: HoverBlockKind::Markdown,
+                    }],
+                    expected_marked_text: "one ยซtwoยป three".to_string(),
+                    expected_styles: vec![HighlightStyle {
+                        weight: Some(Weight::BOLD),
+                        ..Default::default()
+                    }],
+                },
+                // Links
+                Row {
+                    blocks: vec![HoverBlock {
+                        text: "one [two](https://the-url) three".to_string(),
+                        kind: HoverBlockKind::Markdown,
+                    }],
+                    expected_marked_text: "one ยซtwoยป three".to_string(),
+                    expected_styles: vec![HighlightStyle {
+                        underline: Some(Underline {
+                            thickness: 1.0.into(),
+                            ..Default::default()
+                        }),
+                        ..Default::default()
+                    }],
+                },
+                // Lists
+                Row {
+                    blocks: vec![HoverBlock {
+                        text: "
+                            lists:
+                            * one
+                                - a
+                                - b
+                            * two
+                                - [c](https://the-url)
+                                - d"
+                        .unindent(),
+                        kind: HoverBlockKind::Markdown,
+                    }],
+                    expected_marked_text: "
+                        lists:
+                        - one
+                          - a
+                          - b
+                        - two
+                          - ยซcยป
+                          - d"
+                    .unindent(),
+                    expected_styles: vec![HighlightStyle {
+                        underline: Some(Underline {
+                            thickness: 1.0.into(),
+                            ..Default::default()
+                        }),
+                        ..Default::default()
+                    }],
+                },
+                // Multi-paragraph list items
+                Row {
+                    blocks: vec![HoverBlock {
+                        text: "
+                            * one two
+                              three
+
+                            * four five
+                                * six seven
+                                  eight
+
+                                  nine
+                                * ten
+                            * six"
+                            .unindent(),
+                        kind: HoverBlockKind::Markdown,
+                    }],
+                    expected_marked_text: "
+                        - one two three
+                        - four five
+                          - six seven eight
+
+                            nine
+                          - ten
+                        - six"
+                        .unindent(),
+                    expected_styles: vec![HighlightStyle {
+                        underline: Some(Underline {
+                            thickness: 1.0.into(),
+                            ..Default::default()
+                        }),
+                        ..Default::default()
+                    }],
+                },
+            ];
+
+            for Row {
+                blocks,
+                expected_marked_text,
+                expected_styles,
+            } in &rows[0..]
+            {
+                let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
+
+                let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
+                let expected_highlights = ranges
+                    .into_iter()
+                    .zip(expected_styles.iter().cloned())
+                    .collect::<Vec<_>>();
+                assert_eq!(
+                    rendered.text, expected_text,
+                    "wrong text for input {blocks:?}"
+                );
+
+                let rendered_highlights: Vec<_> = rendered
+                    .highlights
+                    .iter()
+                    .filter_map(|(range, highlight)| {
+                        let highlight = highlight.to_highlight_style(&style.syntax)?;
+                        Some((range.clone(), highlight))
+                    })
+                    .collect();
+
+                assert_eq!(
+                    rendered_highlights, expected_highlights,
+                    "wrong highlights for input {blocks:?}"
+                );
+            }
+
+            editor
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Right(
+                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
+                        resolve_provider: Some(true),
+                        ..Default::default()
+                    }),
+                )),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variableห‡ = TestNewType(TestStruct);
+            }
+        "});
+
+        let hint_start_offset = cx.ranges(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variableห‡ = TestNewType(TestStruct);
+            }
+        "})[0]
+            .start;
+        let hint_position = cx.to_lsp(hint_start_offset);
+        let new_type_target_range = cx.lsp_range(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct ยซTestNewTypeยป<T>(T);
+
+            fn main() {
+                let variable = TestNewType(TestStruct);
+            }
+        "});
+        let struct_target_range = cx.lsp_range(indoc! {"
+            struct ยซTestStructยป;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variable = TestNewType(TestStruct);
+            }
+        "});
+
+        let uri = cx.buffer_lsp_url.clone();
+        let new_type_label = "TestNewType";
+        let struct_label = "TestStruct";
+        let entire_hint_label = ": TestNewType<TestStruct>";
+        let closure_uri = uri.clone();
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_uri = closure_uri.clone();
+                async move {
+                    assert_eq!(params.text_document.uri, task_uri);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: hint_position,
+                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+                            value: entire_hint_label.to_string(),
+                            ..Default::default()
+                        }]),
+                        kind: Some(lsp::InlayHintKind::TYPE),
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: Some(false),
+                        padding_right: Some(false),
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let expected_layers = vec![entire_hint_label.to_string()];
+            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+        });
+
+        let inlay_range = cx
+            .ranges(indoc! {"
+                struct TestStruct;
+
+                // ==================
+
+                struct TestNewType<T>(T);
+
+                fn main() {
+                    let variableยซ ยป= TestNewType(TestStruct);
+                }
+        "})
+            .get(0)
+            .cloned()
+            .unwrap();
+        let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column()
+                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
+                        as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                new_type_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+
+        let resolve_closure_uri = uri.clone();
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
+                move |mut hint_to_resolve, _| {
+                    let mut resolved_hint_positions = BTreeSet::new();
+                    let task_uri = resolve_closure_uri.clone();
+                    async move {
+                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
+                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
+
+                        // `: TestNewType<TestStruct>`
+                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
+                            lsp::InlayHintLabelPart {
+                                value: ": ".to_string(),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: new_type_label.to_string(),
+                                location: Some(lsp::Location {
+                                    uri: task_uri.clone(),
+                                    range: new_type_target_range,
+                                }),
+                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
+                                    "A tooltip for `{new_type_label}`"
+                                ))),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: "<".to_string(),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: struct_label.to_string(),
+                                location: Some(lsp::Location {
+                                    uri: task_uri,
+                                    range: struct_target_range,
+                                }),
+                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
+                                    lsp::MarkupContent {
+                                        kind: lsp::MarkupKind::Markdown,
+                                        value: format!("A tooltip for `{struct_label}`"),
+                                    },
+                                )),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: ">".to_string(),
+                                ..Default::default()
+                            },
+                        ]);
+
+                        Ok(hint_to_resolve)
+                    }
+                },
+            )
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                new_type_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground()
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let hover_state = &editor.hover_state;
+            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
+            let popover = hover_state.info_popover.as_ref().unwrap();
+            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+            assert_eq!(
+                popover.symbol_range,
+                RangeInEditor::Inlay(InlayHighlight {
+                    inlay: InlayId::Hint(0),
+                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                    range: ": ".len()..": ".len() + new_type_label.len(),
+                }),
+                "Popover range should match the new type label part"
+            );
+            assert_eq!(
+                popover.parsed_content.text,
+                format!("A tooltip for `{new_type_label}`"),
+                "Rendered text should not anyhow alter backticks"
+            );
+        });
+
+        let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column()
+                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
+                        as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                struct_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground()
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let hover_state = &editor.hover_state;
+            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
+            let popover = hover_state.info_popover.as_ref().unwrap();
+            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+            assert_eq!(
+                popover.symbol_range,
+                RangeInEditor::Inlay(InlayHighlight {
+                    inlay: InlayId::Hint(0),
+                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                    range: ": ".len() + new_type_label.len() + "<".len()
+                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
+                }),
+                "Popover range should match the struct label part"
+            );
+            assert_eq!(
+                popover.parsed_content.text,
+                format!("A tooltip for {struct_label}"),
+                "Rendered markdown element should remove backticks from text"
+            );
+        });
+    }
+}

crates/editor2/src/inlay_hint_cache.rs ๐Ÿ”—

@@ -0,0 +1,3349 @@
+use std::{
+    cmp,
+    ops::{ControlFlow, Range},
+    sync::Arc,
+    time::Duration,
+};
+
+use crate::{
+    display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot,
+};
+use anyhow::Context;
+use clock::Global;
+use futures::future;
+use gpui::{ModelContext, ModelHandle, Task, ViewContext};
+use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
+use parking_lot::RwLock;
+use project::{InlayHint, ResolveState};
+
+use collections::{hash_map, HashMap, HashSet};
+use language::language_settings::InlayHintSettings;
+use smol::lock::Semaphore;
+use sum_tree::Bias;
+use text::{ToOffset, ToPoint};
+use util::post_inc;
+
+pub struct InlayHintCache {
+    hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
+    allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
+    version: usize,
+    pub(super) enabled: bool,
+    update_tasks: HashMap<ExcerptId, TasksForRanges>,
+    lsp_request_limiter: Arc<Semaphore>,
+}
+
+#[derive(Debug)]
+struct TasksForRanges {
+    tasks: Vec<Task<()>>,
+    sorted_ranges: Vec<Range<language::Anchor>>,
+}
+
+#[derive(Debug)]
+pub struct CachedExcerptHints {
+    version: usize,
+    buffer_version: Global,
+    buffer_id: u64,
+    ordered_hints: Vec<InlayId>,
+    hints_by_id: HashMap<InlayId, InlayHint>,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum InvalidationStrategy {
+    RefreshRequested,
+    BufferEdited,
+    None,
+}
+
+#[derive(Debug, Default)]
+pub struct InlaySplice {
+    pub to_remove: Vec<InlayId>,
+    pub to_insert: Vec<Inlay>,
+}
+
+#[derive(Debug)]
+struct ExcerptHintsUpdate {
+    excerpt_id: ExcerptId,
+    remove_from_visible: Vec<InlayId>,
+    remove_from_cache: HashSet<InlayId>,
+    add_to_cache: Vec<InlayHint>,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ExcerptQuery {
+    buffer_id: u64,
+    excerpt_id: ExcerptId,
+    cache_version: usize,
+    invalidate: InvalidationStrategy,
+    reason: &'static str,
+}
+
+impl InvalidationStrategy {
+    fn should_invalidate(&self) -> bool {
+        matches!(
+            self,
+            InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
+        )
+    }
+}
+
+impl TasksForRanges {
+    fn new(query_ranges: QueryRanges, task: Task<()>) -> Self {
+        let mut sorted_ranges = Vec::new();
+        sorted_ranges.extend(query_ranges.before_visible);
+        sorted_ranges.extend(query_ranges.visible);
+        sorted_ranges.extend(query_ranges.after_visible);
+        Self {
+            tasks: vec![task],
+            sorted_ranges,
+        }
+    }
+
+    fn update_cached_tasks(
+        &mut self,
+        buffer_snapshot: &BufferSnapshot,
+        query_ranges: QueryRanges,
+        invalidate: InvalidationStrategy,
+        spawn_task: impl FnOnce(QueryRanges) -> Task<()>,
+    ) {
+        let query_ranges = if invalidate.should_invalidate() {
+            self.tasks.clear();
+            self.sorted_ranges.clear();
+            query_ranges
+        } else {
+            let mut non_cached_query_ranges = query_ranges;
+            non_cached_query_ranges.before_visible = non_cached_query_ranges
+                .before_visible
+                .into_iter()
+                .flat_map(|query_range| {
+                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
+                })
+                .collect();
+            non_cached_query_ranges.visible = non_cached_query_ranges
+                .visible
+                .into_iter()
+                .flat_map(|query_range| {
+                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
+                })
+                .collect();
+            non_cached_query_ranges.after_visible = non_cached_query_ranges
+                .after_visible
+                .into_iter()
+                .flat_map(|query_range| {
+                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
+                })
+                .collect();
+            non_cached_query_ranges
+        };
+
+        if !query_ranges.is_empty() {
+            self.tasks.push(spawn_task(query_ranges));
+        }
+    }
+
+    fn remove_cached_ranges_from_query(
+        &mut self,
+        buffer_snapshot: &BufferSnapshot,
+        query_range: Range<language::Anchor>,
+    ) -> Vec<Range<language::Anchor>> {
+        let mut ranges_to_query = Vec::new();
+        let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
+        for cached_range in self
+            .sorted_ranges
+            .iter_mut()
+            .skip_while(|cached_range| {
+                cached_range
+                    .end
+                    .cmp(&query_range.start, buffer_snapshot)
+                    .is_lt()
+            })
+            .take_while(|cached_range| {
+                cached_range
+                    .start
+                    .cmp(&query_range.end, buffer_snapshot)
+                    .is_le()
+            })
+        {
+            match latest_cached_range {
+                Some(latest_cached_range) => {
+                    if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset
+                    {
+                        ranges_to_query.push(latest_cached_range.end..cached_range.start);
+                        cached_range.start = latest_cached_range.end;
+                    }
+                }
+                None => {
+                    if query_range
+                        .start
+                        .cmp(&cached_range.start, buffer_snapshot)
+                        .is_lt()
+                    {
+                        ranges_to_query.push(query_range.start..cached_range.start);
+                        cached_range.start = query_range.start;
+                    }
+                }
+            }
+            latest_cached_range = Some(cached_range);
+        }
+
+        match latest_cached_range {
+            Some(latest_cached_range) => {
+                if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset {
+                    ranges_to_query.push(latest_cached_range.end..query_range.end);
+                    latest_cached_range.end = query_range.end;
+                }
+            }
+            None => {
+                ranges_to_query.push(query_range.clone());
+                self.sorted_ranges.push(query_range);
+                self.sorted_ranges
+                    .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot));
+            }
+        }
+
+        ranges_to_query
+    }
+
+    fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range<language::Anchor>) {
+        self.sorted_ranges = self
+            .sorted_ranges
+            .drain(..)
+            .filter_map(|mut cached_range| {
+                if cached_range.start.cmp(&range.end, buffer).is_gt()
+                    || cached_range.end.cmp(&range.start, buffer).is_lt()
+                {
+                    Some(vec![cached_range])
+                } else if cached_range.start.cmp(&range.start, buffer).is_ge()
+                    && cached_range.end.cmp(&range.end, buffer).is_le()
+                {
+                    None
+                } else if range.start.cmp(&cached_range.start, buffer).is_ge()
+                    && range.end.cmp(&cached_range.end, buffer).is_le()
+                {
+                    Some(vec![
+                        cached_range.start..range.start,
+                        range.end..cached_range.end,
+                    ])
+                } else if cached_range.start.cmp(&range.start, buffer).is_ge() {
+                    cached_range.start = range.end;
+                    Some(vec![cached_range])
+                } else {
+                    cached_range.end = range.start;
+                    Some(vec![cached_range])
+                }
+            })
+            .flatten()
+            .collect();
+    }
+}
+
+impl InlayHintCache {
+    pub fn new(inlay_hint_settings: InlayHintSettings) -> Self {
+        Self {
+            allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
+            enabled: inlay_hint_settings.enabled,
+            hints: HashMap::default(),
+            update_tasks: HashMap::default(),
+            version: 0,
+            lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)),
+        }
+    }
+
+    pub fn update_settings(
+        &mut self,
+        multi_buffer: &ModelHandle<MultiBuffer>,
+        new_hint_settings: InlayHintSettings,
+        visible_hints: Vec<Inlay>,
+        cx: &mut ViewContext<Editor>,
+    ) -> ControlFlow<Option<InlaySplice>> {
+        let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
+        match (self.enabled, new_hint_settings.enabled) {
+            (false, false) => {
+                self.allowed_hint_kinds = new_allowed_hint_kinds;
+                ControlFlow::Break(None)
+            }
+            (true, true) => {
+                if new_allowed_hint_kinds == self.allowed_hint_kinds {
+                    ControlFlow::Break(None)
+                } else {
+                    let new_splice = self.new_allowed_hint_kinds_splice(
+                        multi_buffer,
+                        &visible_hints,
+                        &new_allowed_hint_kinds,
+                        cx,
+                    );
+                    if new_splice.is_some() {
+                        self.version += 1;
+                        self.allowed_hint_kinds = new_allowed_hint_kinds;
+                    }
+                    ControlFlow::Break(new_splice)
+                }
+            }
+            (true, false) => {
+                self.enabled = new_hint_settings.enabled;
+                self.allowed_hint_kinds = new_allowed_hint_kinds;
+                if self.hints.is_empty() {
+                    ControlFlow::Break(None)
+                } else {
+                    self.clear();
+                    ControlFlow::Break(Some(InlaySplice {
+                        to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
+                        to_insert: Vec::new(),
+                    }))
+                }
+            }
+            (false, true) => {
+                self.enabled = new_hint_settings.enabled;
+                self.allowed_hint_kinds = new_allowed_hint_kinds;
+                ControlFlow::Continue(())
+            }
+        }
+    }
+
+    pub fn spawn_hint_refresh(
+        &mut self,
+        reason: &'static str,
+        excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+        invalidate: InvalidationStrategy,
+        cx: &mut ViewContext<Editor>,
+    ) -> Option<InlaySplice> {
+        if !self.enabled {
+            return None;
+        }
+
+        let mut invalidated_hints = Vec::new();
+        if invalidate.should_invalidate() {
+            self.update_tasks
+                .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
+            self.hints.retain(|cached_excerpt, cached_hints| {
+                let retain = excerpts_to_query.contains_key(cached_excerpt);
+                if !retain {
+                    invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied());
+                }
+                retain
+            });
+        }
+        if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
+            return None;
+        }
+
+        let cache_version = self.version + 1;
+        cx.spawn(|editor, mut cx| async move {
+            editor
+                .update(&mut cx, |editor, cx| {
+                    spawn_new_update_tasks(
+                        editor,
+                        reason,
+                        excerpts_to_query,
+                        invalidate,
+                        cache_version,
+                        cx,
+                    )
+                })
+                .ok();
+        })
+        .detach();
+
+        if invalidated_hints.is_empty() {
+            None
+        } else {
+            Some(InlaySplice {
+                to_remove: invalidated_hints,
+                to_insert: Vec::new(),
+            })
+        }
+    }
+
+    fn new_allowed_hint_kinds_splice(
+        &self,
+        multi_buffer: &ModelHandle<MultiBuffer>,
+        visible_hints: &[Inlay],
+        new_kinds: &HashSet<Option<InlayHintKind>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> Option<InlaySplice> {
+        let old_kinds = &self.allowed_hint_kinds;
+        if new_kinds == old_kinds {
+            return None;
+        }
+
+        let mut to_remove = Vec::new();
+        let mut to_insert = Vec::new();
+        let mut shown_hints_to_remove = visible_hints.iter().fold(
+            HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
+            |mut current_hints, inlay| {
+                current_hints
+                    .entry(inlay.position.excerpt_id)
+                    .or_default()
+                    .push((inlay.position, inlay.id));
+                current_hints
+            },
+        );
+
+        let multi_buffer = multi_buffer.read(cx);
+        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+
+        for (excerpt_id, excerpt_cached_hints) in &self.hints {
+            let shown_excerpt_hints_to_remove =
+                shown_hints_to_remove.entry(*excerpt_id).or_default();
+            let excerpt_cached_hints = excerpt_cached_hints.read();
+            let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
+            shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
+                let Some(buffer) = shown_anchor
+                    .buffer_id
+                    .and_then(|buffer_id| multi_buffer.buffer(buffer_id))
+                else {
+                    return false;
+                };
+                let buffer_snapshot = buffer.read(cx).snapshot();
+                loop {
+                    match excerpt_cache.peek() {
+                        Some(&cached_hint_id) => {
+                            let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
+                            if cached_hint_id == shown_hint_id {
+                                excerpt_cache.next();
+                                return !new_kinds.contains(&cached_hint.kind);
+                            }
+
+                            match cached_hint
+                                .position
+                                .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
+                            {
+                                cmp::Ordering::Less | cmp::Ordering::Equal => {
+                                    if !old_kinds.contains(&cached_hint.kind)
+                                        && new_kinds.contains(&cached_hint.kind)
+                                    {
+                                        to_insert.push(Inlay::hint(
+                                            cached_hint_id.id(),
+                                            multi_buffer_snapshot.anchor_in_excerpt(
+                                                *excerpt_id,
+                                                cached_hint.position,
+                                            ),
+                                            &cached_hint,
+                                        ));
+                                    }
+                                    excerpt_cache.next();
+                                }
+                                cmp::Ordering::Greater => return true,
+                            }
+                        }
+                        None => return true,
+                    }
+                }
+            });
+
+            for cached_hint_id in excerpt_cache {
+                let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
+                let cached_hint_kind = maybe_missed_cached_hint.kind;
+                if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
+                    to_insert.push(Inlay::hint(
+                        cached_hint_id.id(),
+                        multi_buffer_snapshot
+                            .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
+                        &maybe_missed_cached_hint,
+                    ));
+                }
+            }
+        }
+
+        to_remove.extend(
+            shown_hints_to_remove
+                .into_values()
+                .flatten()
+                .map(|(_, hint_id)| hint_id),
+        );
+        if to_remove.is_empty() && to_insert.is_empty() {
+            None
+        } else {
+            Some(InlaySplice {
+                to_remove,
+                to_insert,
+            })
+        }
+    }
+
+    pub fn remove_excerpts(&mut self, excerpts_removed: Vec<ExcerptId>) -> Option<InlaySplice> {
+        let mut to_remove = Vec::new();
+        for excerpt_to_remove in excerpts_removed {
+            self.update_tasks.remove(&excerpt_to_remove);
+            if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
+                let cached_hints = cached_hints.read();
+                to_remove.extend(cached_hints.ordered_hints.iter().copied());
+            }
+        }
+        if to_remove.is_empty() {
+            None
+        } else {
+            self.version += 1;
+            Some(InlaySplice {
+                to_remove,
+                to_insert: Vec::new(),
+            })
+        }
+    }
+
+    pub fn clear(&mut self) {
+        if !self.update_tasks.is_empty() || !self.hints.is_empty() {
+            self.version += 1;
+        }
+        self.update_tasks.clear();
+        self.hints.clear();
+    }
+
+    pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option<InlayHint> {
+        self.hints
+            .get(&excerpt_id)?
+            .read()
+            .hints_by_id
+            .get(&hint_id)
+            .cloned()
+    }
+
+    pub fn hints(&self) -> Vec<InlayHint> {
+        let mut hints = Vec::new();
+        for excerpt_hints in self.hints.values() {
+            let excerpt_hints = excerpt_hints.read();
+            hints.extend(
+                excerpt_hints
+                    .ordered_hints
+                    .iter()
+                    .map(|id| &excerpt_hints.hints_by_id[id])
+                    .cloned(),
+            );
+        }
+        hints
+    }
+
+    pub fn version(&self) -> usize {
+        self.version
+    }
+
+    pub fn spawn_hint_resolve(
+        &self,
+        buffer_id: u64,
+        excerpt_id: ExcerptId,
+        id: InlayId,
+        cx: &mut ViewContext<'_, '_, Editor>,
+    ) {
+        if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
+            let mut guard = excerpt_hints.write();
+            if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
+                if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state {
+                    let hint_to_resolve = cached_hint.clone();
+                    let server_id = *server_id;
+                    cached_hint.resolve_state = ResolveState::Resolving;
+                    drop(guard);
+                    cx.spawn(|editor, mut cx| async move {
+                        let resolved_hint_task = editor.update(&mut cx, |editor, cx| {
+                            editor
+                                .buffer()
+                                .read(cx)
+                                .buffer(buffer_id)
+                                .and_then(|buffer| {
+                                    let project = editor.project.as_ref()?;
+                                    Some(project.update(cx, |project, cx| {
+                                        project.resolve_inlay_hint(
+                                            hint_to_resolve,
+                                            buffer,
+                                            server_id,
+                                            cx,
+                                        )
+                                    }))
+                                })
+                        })?;
+                        if let Some(resolved_hint_task) = resolved_hint_task {
+                            let mut resolved_hint =
+                                resolved_hint_task.await.context("hint resolve task")?;
+                            editor.update(&mut cx, |editor, _| {
+                                if let Some(excerpt_hints) =
+                                    editor.inlay_hint_cache.hints.get(&excerpt_id)
+                                {
+                                    let mut guard = excerpt_hints.write();
+                                    if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
+                                        if cached_hint.resolve_state == ResolveState::Resolving {
+                                            resolved_hint.resolve_state = ResolveState::Resolved;
+                                            *cached_hint = resolved_hint;
+                                        }
+                                    }
+                                }
+                            })?;
+                        }
+
+                        anyhow::Ok(())
+                    })
+                    .detach_and_log_err(cx);
+                }
+            }
+        }
+    }
+}
+
+fn spawn_new_update_tasks(
+    editor: &mut Editor,
+    reason: &'static str,
+    excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+    invalidate: InvalidationStrategy,
+    update_cache_version: usize,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) {
+    let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
+    for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in
+        excerpts_to_query
+    {
+        if excerpt_visible_range.is_empty() {
+            continue;
+        }
+        let buffer = excerpt_buffer.read(cx);
+        let buffer_id = buffer.remote_id();
+        let buffer_snapshot = buffer.snapshot();
+        if buffer_snapshot
+            .version()
+            .changed_since(&new_task_buffer_version)
+        {
+            continue;
+        }
+
+        let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
+        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+            let cached_excerpt_hints = cached_excerpt_hints.read();
+            let cached_buffer_version = &cached_excerpt_hints.buffer_version;
+            if cached_excerpt_hints.version > update_cache_version
+                || cached_buffer_version.changed_since(&new_task_buffer_version)
+            {
+                continue;
+            }
+        };
+
+        let (multi_buffer_snapshot, Some(query_ranges)) =
+            editor.buffer.update(cx, |multi_buffer, cx| {
+                (
+                    multi_buffer.snapshot(cx),
+                    determine_query_ranges(
+                        multi_buffer,
+                        excerpt_id,
+                        &excerpt_buffer,
+                        excerpt_visible_range,
+                        cx,
+                    ),
+                )
+            })
+        else {
+            return;
+        };
+        let query = ExcerptQuery {
+            buffer_id,
+            excerpt_id,
+            cache_version: update_cache_version,
+            invalidate,
+            reason,
+        };
+
+        let new_update_task = |query_ranges| {
+            new_update_task(
+                query,
+                query_ranges,
+                multi_buffer_snapshot,
+                buffer_snapshot.clone(),
+                Arc::clone(&visible_hints),
+                cached_excerpt_hints,
+                Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter),
+                cx,
+            )
+        };
+
+        match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
+            hash_map::Entry::Occupied(mut o) => {
+                o.get_mut().update_cached_tasks(
+                    &buffer_snapshot,
+                    query_ranges,
+                    invalidate,
+                    new_update_task,
+                );
+            }
+            hash_map::Entry::Vacant(v) => {
+                v.insert(TasksForRanges::new(
+                    query_ranges.clone(),
+                    new_update_task(query_ranges),
+                ));
+            }
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+struct QueryRanges {
+    before_visible: Vec<Range<language::Anchor>>,
+    visible: Vec<Range<language::Anchor>>,
+    after_visible: Vec<Range<language::Anchor>>,
+}
+
+impl QueryRanges {
+    fn is_empty(&self) -> bool {
+        self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty()
+    }
+}
+
+fn determine_query_ranges(
+    multi_buffer: &mut MultiBuffer,
+    excerpt_id: ExcerptId,
+    excerpt_buffer: &ModelHandle<Buffer>,
+    excerpt_visible_range: Range<usize>,
+    cx: &mut ModelContext<'_, MultiBuffer>,
+) -> Option<QueryRanges> {
+    let full_excerpt_range = multi_buffer
+        .excerpts_for_buffer(excerpt_buffer, cx)
+        .into_iter()
+        .find(|(id, _)| id == &excerpt_id)
+        .map(|(_, range)| range.context)?;
+    let buffer = excerpt_buffer.read(cx);
+    let snapshot = buffer.snapshot();
+    let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start;
+
+    let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end {
+        return None;
+    } else {
+        vec![
+            buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left))
+                ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)),
+        ]
+    };
+
+    let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot);
+    let after_visible_range_start = excerpt_visible_range
+        .end
+        .saturating_add(1)
+        .min(full_excerpt_range_end_offset)
+        .min(buffer.len());
+    let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset {
+        Vec::new()
+    } else {
+        let after_range_end_offset = after_visible_range_start
+            .saturating_add(excerpt_visible_len)
+            .min(full_excerpt_range_end_offset)
+            .min(buffer.len());
+        vec![
+            buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left))
+                ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)),
+        ]
+    };
+
+    let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot);
+    let before_visible_range_end = excerpt_visible_range
+        .start
+        .saturating_sub(1)
+        .max(full_excerpt_range_start_offset);
+    let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset {
+        Vec::new()
+    } else {
+        let before_range_start_offset = before_visible_range_end
+            .saturating_sub(excerpt_visible_len)
+            .max(full_excerpt_range_start_offset);
+        vec![
+            buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left))
+                ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)),
+        ]
+    };
+
+    Some(QueryRanges {
+        before_visible: before_visible_range,
+        visible: visible_range,
+        after_visible: after_visible_range,
+    })
+}
+
+const MAX_CONCURRENT_LSP_REQUESTS: usize = 5;
+const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400;
+
+fn new_update_task(
+    query: ExcerptQuery,
+    query_ranges: QueryRanges,
+    multi_buffer_snapshot: MultiBufferSnapshot,
+    buffer_snapshot: BufferSnapshot,
+    visible_hints: Arc<Vec<Inlay>>,
+    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+    lsp_request_limiter: Arc<Semaphore>,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) -> Task<()> {
+    cx.spawn(|editor, mut cx| async move {
+        let closure_cx = cx.clone();
+        let fetch_and_update_hints = |invalidate, range| {
+            fetch_and_update_hints(
+                editor.clone(),
+                multi_buffer_snapshot.clone(),
+                buffer_snapshot.clone(),
+                Arc::clone(&visible_hints),
+                cached_excerpt_hints.as_ref().map(Arc::clone),
+                query,
+                invalidate,
+                range,
+                Arc::clone(&lsp_request_limiter),
+                closure_cx.clone(),
+            )
+        };
+        let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map(
+            |visible_range| async move {
+                (
+                    visible_range.clone(),
+                    fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range)
+                        .await,
+                )
+            },
+        ))
+        .await;
+
+        let hint_delay = cx.background().timer(Duration::from_millis(
+            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS,
+        ));
+
+        let mut query_range_failed = |range: &Range<language::Anchor>, e: anyhow::Error| {
+            log::error!("inlay hint update task for range {range:?} failed: {e:#}");
+            editor
+                .update(&mut cx, |editor, _| {
+                    if let Some(task_ranges) = editor
+                        .inlay_hint_cache
+                        .update_tasks
+                        .get_mut(&query.excerpt_id)
+                    {
+                        task_ranges.invalidate_range(&buffer_snapshot, &range);
+                    }
+                })
+                .ok()
+        };
+
+        for (range, result) in visible_range_update_results {
+            if let Err(e) = result {
+                query_range_failed(&range, e);
+            }
+        }
+
+        hint_delay.await;
+        let invisible_range_update_results = future::join_all(
+            query_ranges
+                .before_visible
+                .into_iter()
+                .chain(query_ranges.after_visible.into_iter())
+                .map(|invisible_range| async move {
+                    (
+                        invisible_range.clone(),
+                        fetch_and_update_hints(false, invisible_range).await,
+                    )
+                }),
+        )
+        .await;
+        for (range, result) in invisible_range_update_results {
+            if let Err(e) = result {
+                query_range_failed(&range, e);
+            }
+        }
+    })
+}
+
+async fn fetch_and_update_hints(
+    editor: gpui::WeakViewHandle<Editor>,
+    multi_buffer_snapshot: MultiBufferSnapshot,
+    buffer_snapshot: BufferSnapshot,
+    visible_hints: Arc<Vec<Inlay>>,
+    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+    query: ExcerptQuery,
+    invalidate: bool,
+    fetch_range: Range<language::Anchor>,
+    lsp_request_limiter: Arc<Semaphore>,
+    mut cx: gpui::AsyncAppContext,
+) -> anyhow::Result<()> {
+    let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
+        (None, false)
+    } else {
+        match lsp_request_limiter.try_acquire() {
+            Some(guard) => (Some(guard), false),
+            None => (Some(lsp_request_limiter.acquire().await), true),
+        }
+    };
+    let fetch_range_to_log =
+        fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
+    let inlay_hints_fetch_task = editor
+        .update(&mut cx, |editor, cx| {
+            if got_throttled {
+                let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) {
+                    Some((_, _, current_visible_range)) => {
+                        let visible_offset_length = current_visible_range.len();
+                        let double_visible_range = current_visible_range
+                            .start
+                            .saturating_sub(visible_offset_length)
+                            ..current_visible_range
+                                .end
+                                .saturating_add(visible_offset_length)
+                                .min(buffer_snapshot.len());
+                        !double_visible_range
+                            .contains(&fetch_range.start.to_offset(&buffer_snapshot))
+                            && !double_visible_range
+                                .contains(&fetch_range.end.to_offset(&buffer_snapshot))
+                    },
+                    None => true,
+                };
+                if query_not_around_visible_range {
+                    log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
+                    if let Some(task_ranges) = editor
+                        .inlay_hint_cache
+                        .update_tasks
+                        .get_mut(&query.excerpt_id)
+                    {
+                        task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
+                    }
+                    return None;
+                }
+            }
+            editor
+                .buffer()
+                .read(cx)
+                .buffer(query.buffer_id)
+                .and_then(|buffer| {
+                    let project = editor.project.as_ref()?;
+                    Some(project.update(cx, |project, cx| {
+                        project.inlay_hints(buffer, fetch_range.clone(), cx)
+                    }))
+                })
+        })
+        .ok()
+        .flatten();
+    let new_hints = match inlay_hints_fetch_task {
+        Some(fetch_task) => {
+            log::debug!(
+                "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
+                query_reason = query.reason,
+            );
+            log::trace!(
+                "Currently visible hints: {visible_hints:?}, cached hints present: {}",
+                cached_excerpt_hints.is_some(),
+            );
+            fetch_task.await.context("inlay hint fetch task")?
+        }
+        None => return Ok(()),
+    };
+    drop(lsp_request_guard);
+    log::debug!(
+        "Fetched {} hints for range {fetch_range_to_log:?}",
+        new_hints.len()
+    );
+    log::trace!("Fetched hints: {new_hints:?}");
+
+    let background_task_buffer_snapshot = buffer_snapshot.clone();
+    let backround_fetch_range = fetch_range.clone();
+    let new_update = cx
+        .background()
+        .spawn(async move {
+            calculate_hint_updates(
+                query.excerpt_id,
+                invalidate,
+                backround_fetch_range,
+                new_hints,
+                &background_task_buffer_snapshot,
+                cached_excerpt_hints,
+                &visible_hints,
+            )
+        })
+        .await;
+    if let Some(new_update) = new_update {
+        log::debug!(
+            "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
+            new_update.remove_from_visible.len(),
+            new_update.remove_from_cache.len(),
+            new_update.add_to_cache.len()
+        );
+        log::trace!("New update: {new_update:?}");
+        editor
+            .update(&mut cx, |editor, cx| {
+                apply_hint_update(
+                    editor,
+                    new_update,
+                    query,
+                    invalidate,
+                    buffer_snapshot,
+                    multi_buffer_snapshot,
+                    cx,
+                );
+            })
+            .ok();
+    }
+    Ok(())
+}
+
+fn calculate_hint_updates(
+    excerpt_id: ExcerptId,
+    invalidate: bool,
+    fetch_range: Range<language::Anchor>,
+    new_excerpt_hints: Vec<InlayHint>,
+    buffer_snapshot: &BufferSnapshot,
+    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+    visible_hints: &[Inlay],
+) -> Option<ExcerptHintsUpdate> {
+    let mut add_to_cache = Vec::<InlayHint>::new();
+    let mut excerpt_hints_to_persist = HashMap::default();
+    for new_hint in new_excerpt_hints {
+        if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
+            continue;
+        }
+        let missing_from_cache = match &cached_excerpt_hints {
+            Some(cached_excerpt_hints) => {
+                let cached_excerpt_hints = cached_excerpt_hints.read();
+                match cached_excerpt_hints
+                    .ordered_hints
+                    .binary_search_by(|probe| {
+                        cached_excerpt_hints.hints_by_id[probe]
+                            .position
+                            .cmp(&new_hint.position, buffer_snapshot)
+                    }) {
+                    Ok(ix) => {
+                        let mut missing_from_cache = true;
+                        for id in &cached_excerpt_hints.ordered_hints[ix..] {
+                            let cached_hint = &cached_excerpt_hints.hints_by_id[id];
+                            if new_hint
+                                .position
+                                .cmp(&cached_hint.position, buffer_snapshot)
+                                .is_gt()
+                            {
+                                break;
+                            }
+                            if cached_hint == &new_hint {
+                                excerpt_hints_to_persist.insert(*id, cached_hint.kind);
+                                missing_from_cache = false;
+                            }
+                        }
+                        missing_from_cache
+                    }
+                    Err(_) => true,
+                }
+            }
+            None => true,
+        };
+        if missing_from_cache {
+            add_to_cache.push(new_hint);
+        }
+    }
+
+    let mut remove_from_visible = Vec::new();
+    let mut remove_from_cache = HashSet::default();
+    if invalidate {
+        remove_from_visible.extend(
+            visible_hints
+                .iter()
+                .filter(|hint| hint.position.excerpt_id == excerpt_id)
+                .map(|inlay_hint| inlay_hint.id)
+                .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
+        );
+
+        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+            let cached_excerpt_hints = cached_excerpt_hints.read();
+            remove_from_cache.extend(
+                cached_excerpt_hints
+                    .ordered_hints
+                    .iter()
+                    .filter(|cached_inlay_id| {
+                        !excerpt_hints_to_persist.contains_key(cached_inlay_id)
+                    })
+                    .copied(),
+            );
+        }
+    }
+
+    if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
+        None
+    } else {
+        Some(ExcerptHintsUpdate {
+            excerpt_id,
+            remove_from_visible,
+            remove_from_cache,
+            add_to_cache,
+        })
+    }
+}
+
+fn contains_position(
+    range: &Range<language::Anchor>,
+    position: language::Anchor,
+    buffer_snapshot: &BufferSnapshot,
+) -> bool {
+    range.start.cmp(&position, buffer_snapshot).is_le()
+        && range.end.cmp(&position, buffer_snapshot).is_ge()
+}
+
+fn apply_hint_update(
+    editor: &mut Editor,
+    new_update: ExcerptHintsUpdate,
+    query: ExcerptQuery,
+    invalidate: bool,
+    buffer_snapshot: BufferSnapshot,
+    multi_buffer_snapshot: MultiBufferSnapshot,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) {
+    let cached_excerpt_hints = editor
+        .inlay_hint_cache
+        .hints
+        .entry(new_update.excerpt_id)
+        .or_insert_with(|| {
+            Arc::new(RwLock::new(CachedExcerptHints {
+                version: query.cache_version,
+                buffer_version: buffer_snapshot.version().clone(),
+                buffer_id: query.buffer_id,
+                ordered_hints: Vec::new(),
+                hints_by_id: HashMap::default(),
+            }))
+        });
+    let mut cached_excerpt_hints = cached_excerpt_hints.write();
+    match query.cache_version.cmp(&cached_excerpt_hints.version) {
+        cmp::Ordering::Less => return,
+        cmp::Ordering::Greater | cmp::Ordering::Equal => {
+            cached_excerpt_hints.version = query.cache_version;
+        }
+    }
+
+    let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
+    cached_excerpt_hints
+        .ordered_hints
+        .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
+    cached_excerpt_hints
+        .hints_by_id
+        .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
+    let mut splice = InlaySplice {
+        to_remove: new_update.remove_from_visible,
+        to_insert: Vec::new(),
+    };
+    for new_hint in new_update.add_to_cache {
+        let insert_position = match cached_excerpt_hints
+            .ordered_hints
+            .binary_search_by(|probe| {
+                cached_excerpt_hints.hints_by_id[probe]
+                    .position
+                    .cmp(&new_hint.position, &buffer_snapshot)
+            }) {
+            Ok(i) => {
+                let mut insert_position = Some(i);
+                for id in &cached_excerpt_hints.ordered_hints[i..] {
+                    let cached_hint = &cached_excerpt_hints.hints_by_id[id];
+                    if new_hint
+                        .position
+                        .cmp(&cached_hint.position, &buffer_snapshot)
+                        .is_gt()
+                    {
+                        break;
+                    }
+                    if cached_hint.text() == new_hint.text() {
+                        insert_position = None;
+                        break;
+                    }
+                }
+                insert_position
+            }
+            Err(i) => Some(i),
+        };
+
+        if let Some(insert_position) = insert_position {
+            let new_inlay_id = post_inc(&mut editor.next_inlay_id);
+            if editor
+                .inlay_hint_cache
+                .allowed_hint_kinds
+                .contains(&new_hint.kind)
+            {
+                let new_hint_position =
+                    multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position);
+                splice
+                    .to_insert
+                    .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
+            }
+            let new_id = InlayId::Hint(new_inlay_id);
+            cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
+            cached_excerpt_hints
+                .ordered_hints
+                .insert(insert_position, new_id);
+            cached_inlays_changed = true;
+        }
+    }
+    cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
+    drop(cached_excerpt_hints);
+
+    if invalidate {
+        let mut outdated_excerpt_caches = HashSet::default();
+        for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
+            let excerpt_hints = excerpt_hints.read();
+            if excerpt_hints.buffer_id == query.buffer_id
+                && excerpt_id != &query.excerpt_id
+                && buffer_snapshot
+                    .version()
+                    .changed_since(&excerpt_hints.buffer_version)
+            {
+                outdated_excerpt_caches.insert(*excerpt_id);
+                splice
+                    .to_remove
+                    .extend(excerpt_hints.ordered_hints.iter().copied());
+            }
+        }
+        cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
+        editor
+            .inlay_hint_cache
+            .hints
+            .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
+    }
+
+    let InlaySplice {
+        to_remove,
+        to_insert,
+    } = splice;
+    let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty();
+    if cached_inlays_changed || displayed_inlays_changed {
+        editor.inlay_hint_cache.version += 1;
+    }
+    if displayed_inlays_changed {
+        editor.splice_inlay_hints(to_remove, to_insert, cx)
+    }
+}
+
+#[cfg(test)]
+pub mod tests {
+    use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
+
+    use crate::{
+        scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
+        serde_json::json,
+        ExcerptRange,
+    };
+    use futures::StreamExt;
+    use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
+    use itertools::Itertools;
+    use language::{
+        language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
+    };
+    use lsp::FakeLanguageServer;
+    use parking_lot::Mutex;
+    use project::{FakeFs, Project};
+    use settings::SettingsStore;
+    use text::{Point, ToPoint};
+    use workspace::Workspace;
+
+    use crate::editor_tests::update_test_language_settings;
+
+    use super::*;
+
+    #[gpui::test]
+    async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    let current_call_id =
+                        Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
+                    for _ in 0..2 {
+                        let mut i = current_call_id;
+                        loop {
+                            new_hints.push(lsp::InlayHint {
+                                position: lsp::Position::new(0, i),
+                                label: lsp::InlayHintLabel::String(i.to_string()),
+                                kind: None,
+                                text_edits: None,
+                                tooltip: None,
+                                padding_left: None,
+                                padding_right: None,
+                                data: None,
+                            });
+                            if i == 0 {
+                                break;
+                            }
+                            i -= 1;
+                        }
+                    }
+
+                    Ok(Some(new_hints))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        let mut edits_made = 1;
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["0".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+            editor.handle_input("some change", cx);
+            edits_made += 1;
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["0".to_string(), "1".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get new hints after an edit"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        edits_made += 1;
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get new hints after hint refresh/ request"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    let current_call_id =
+                        Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, current_call_id),
+                        label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        let mut edits_made = 1;
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["0".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        let progress_token = "test_progress_token";
+        fake_server
+            .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
+                token: lsp::ProgressToken::String(progress_token.to_string()),
+            })
+            .await
+            .expect("work done progress create request failed");
+        cx.foreground().run_until_parked();
+        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
+            token: lsp::ProgressToken::String(progress_token.to_string()),
+            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
+                lsp::WorkDoneProgressBegin::default(),
+            )),
+        });
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["0".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should not update hints while the work task is running"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                edits_made,
+                "Should not update the cache while the work task is running"
+            );
+        });
+
+        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
+            token: lsp::ProgressToken::String(progress_token.to_string()),
+            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
+                lsp::WorkDoneProgressEnd::default(),
+            )),
+        });
+        cx.foreground().run_until_parked();
+
+        edits_made += 1;
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["1".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "New hints should be queried after the work task is done"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                edits_made,
+                "Cache version should udpate once after the work task is done"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+                    "/a",
+                    json!({
+                        "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                        "other.md": "Test md file with some text",
+                    }),
+                )
+                .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let mut rs_fake_servers = None;
+        let mut md_fake_servers = None;
+        for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
+            let mut language = Language::new(
+                LanguageConfig {
+                    name: name.into(),
+                    path_suffixes: vec![path_suffix.to_string()],
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            );
+            let fake_servers = language
+                .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                    name,
+                    capabilities: lsp::ServerCapabilities {
+                        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                }))
+                .await;
+            match name {
+                "Rust" => rs_fake_servers = Some(fake_servers),
+                "Markdown" => md_fake_servers = Some(fake_servers),
+                _ => unreachable!(),
+            }
+            project.update(cx, |project, _| {
+                project.languages().add(Arc::new(language));
+            });
+        }
+
+        let _rs_buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/main.rs", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
+        let rs_editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
+        rs_fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                    );
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        rs_editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["0".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                1,
+                "Rust editor update the cache version after every cache/view change"
+            );
+        });
+
+        cx.foreground().run_until_parked();
+        let _md_buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/other.md", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
+        let md_editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "other.md"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let md_lsp_request_count = Arc::new(AtomicU32::new(0));
+        md_fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/other.md").unwrap(),
+                    );
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        md_editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["0".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Markdown editor should have a separate verison, repeating Rust editor rules"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, 1);
+        });
+
+        rs_editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+            editor.handle_input("some rs change", cx);
+        });
+        cx.foreground().run_until_parked();
+        rs_editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["1".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Rust inlay cache should change after the edit"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                2,
+                "Every time hint cache changes, cache version should be incremented"
+            );
+        });
+        md_editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["0".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Markdown editor should not be affected by Rust editor changes"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, 1);
+        });
+
+        md_editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+            editor.handle_input("some md change", cx);
+        });
+        cx.foreground().run_until_parked();
+        md_editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["1".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Rust editor should not be affected by Markdown editor changes"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, 2);
+        });
+        rs_editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["1".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Markdown editor should also change independently"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, 2);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let another_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+                async move {
+                    Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    Ok(Some(vec![
+                        lsp::InlayHint {
+                            position: lsp::Position::new(0, 1),
+                            label: lsp::InlayHintLabel::String("type hint".to_string()),
+                            kind: Some(lsp::InlayHintKind::TYPE),
+                            text_edits: None,
+                            tooltip: None,
+                            padding_left: None,
+                            padding_right: None,
+                            data: None,
+                        },
+                        lsp::InlayHint {
+                            position: lsp::Position::new(0, 2),
+                            label: lsp::InlayHintLabel::String("parameter hint".to_string()),
+                            kind: Some(lsp::InlayHintKind::PARAMETER),
+                            text_edits: None,
+                            tooltip: None,
+                            padding_left: None,
+                            padding_right: None,
+                            data: None,
+                        },
+                        lsp::InlayHint {
+                            position: lsp::Position::new(0, 3),
+                            label: lsp::InlayHintLabel::String("other hint".to_string()),
+                            kind: None,
+                            text_edits: None,
+                            tooltip: None,
+                            padding_left: None,
+                            padding_right: None,
+                            data: None,
+                        },
+                    ]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        let mut edits_made = 1;
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                1,
+                "Should query new hints once"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(
+                vec!["other hint".to_string(), "type hint".to_string()],
+                visible_hint_labels(editor, cx)
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should load new hints twice"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+                "Cached hints should not change due to allowed hint kinds settings update"
+            );
+            assert_eq!(
+                vec!["other hint".to_string(), "type hint".to_string()],
+                visible_hint_labels(editor, cx)
+            );
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                edits_made,
+                "Should not update cache version due to new loaded hints being the same"
+            );
+        });
+
+        for (new_allowed_hint_kinds, expected_visible_hints) in [
+            (HashSet::from_iter([None]), vec!["other hint".to_string()]),
+            (
+                HashSet::from_iter([Some(InlayHintKind::Type)]),
+                vec!["type hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([Some(InlayHintKind::Parameter)]),
+                vec!["parameter hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([None, Some(InlayHintKind::Type)]),
+                vec!["other hint".to_string(), "type hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
+                vec!["other hint".to_string(), "parameter hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
+                vec!["parameter hint".to_string(), "type hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([
+                    None,
+                    Some(InlayHintKind::Type),
+                    Some(InlayHintKind::Parameter),
+                ]),
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+            ),
+        ] {
+            edits_made += 1;
+            update_test_language_settings(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: true,
+                    show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                    show_parameter_hints: new_allowed_hint_kinds
+                        .contains(&Some(InlayHintKind::Parameter)),
+                    show_other_hints: new_allowed_hint_kinds.contains(&None),
+                })
+            });
+            cx.foreground().run_until_parked();
+            editor.update(cx, |editor, cx| {
+                assert_eq!(
+                    lsp_request_count.load(Ordering::Relaxed),
+                    2,
+                    "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                assert_eq!(
+                    vec![
+                        "other hint".to_string(),
+                        "parameter hint".to_string(),
+                        "type hint".to_string(),
+                    ],
+                    cached_hint_labels(editor),
+                    "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                assert_eq!(
+                    expected_visible_hints,
+                    visible_hint_labels(editor, cx),
+                    "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                let inlay_cache = editor.inlay_hint_cache();
+                assert_eq!(
+                    inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
+                    "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                assert_eq!(
+                    inlay_cache.version, edits_made,
+                    "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
+                );
+            });
+        }
+
+        edits_made += 1;
+        let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
+        update_test_language_settings(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: false,
+                show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: another_allowed_hint_kinds
+                    .contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: another_allowed_hint_kinds.contains(&None),
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should not load new hints when hints got disabled"
+            );
+            assert!(
+                cached_hint_labels(editor).is_empty(),
+                "Should clear the cache when hints got disabled"
+            );
+            assert!(
+                visible_hint_labels(editor, cx).is_empty(),
+                "Should clear visible hints when hints got disabled"
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
+                "Should update its allowed hint kinds even when hints got disabled"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor should update the cache version after hints got disabled"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should not load new hints when they got disabled"
+            );
+            assert!(cached_hint_labels(editor).is_empty());
+            assert!(visible_hint_labels(editor, cx).is_empty());
+            assert_eq!(
+                editor.inlay_hint_cache().version, edits_made,
+                "The editor should not update the cache version after /refresh query without updates"
+            );
+        });
+
+        let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
+        edits_made += 1;
+        update_test_language_settings(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: final_allowed_hint_kinds
+                    .contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: final_allowed_hint_kinds.contains(&None),
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                3,
+                "Should query for new hints when they got reenabled"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+                "Should get its cached hints fully repopulated after the hints got reenabled"
+            );
+            assert_eq!(
+                vec!["parameter hint".to_string()],
+                visible_hint_labels(editor, cx),
+                "Should get its visible hints repopulated and filtered after the h"
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
+                "Cache should update editor settings when hints got reenabled"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "Cache should update its version after hints got reenabled"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                4,
+                "Should query for new hints again"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+            );
+            assert_eq!(
+                vec!["parameter hint".to_string()],
+                visible_hint_labels(editor, cx),
+            );
+            assert_eq!(editor.inlay_hint_cache().version, edits_made);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let fake_server = Arc::new(fake_server);
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let another_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+                async move {
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+
+        let mut expected_changes = Vec::new();
+        for change_after_opening in [
+            "initial change #1",
+            "initial change #2",
+            "initial change #3",
+        ] {
+            editor.update(cx, |editor, cx| {
+                editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                editor.handle_input(change_after_opening, cx);
+            });
+            expected_changes.push(change_after_opening);
+        }
+
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let current_text = editor.text(cx);
+            for change in &expected_changes {
+                assert!(
+                    current_text.contains(change),
+                    "Should apply all changes made"
+                );
+            }
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should query new hints twice: for editor init and for the last edit that interrupted all others"
+            );
+            let expected_hints = vec!["2".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last edit landed only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(
+                editor.inlay_hint_cache().version, 1,
+                "Only one update should be registered in the cache after all cancellations"
+            );
+        });
+
+        let mut edits = Vec::new();
+        for async_later_change in [
+            "another change #1",
+            "another change #2",
+            "another change #3",
+        ] {
+            expected_changes.push(async_later_change);
+            let task_editor = editor.clone();
+            let mut task_cx = cx.clone();
+            edits.push(cx.foreground().spawn(async move {
+                task_editor.update(&mut task_cx, |editor, cx| {
+                    editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                    editor.handle_input(async_later_change, cx);
+                });
+            }));
+        }
+        let _ = future::join_all(edits).await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let current_text = editor.text(cx);
+            for change in &expected_changes {
+                assert!(
+                    current_text.contains(change),
+                    "Should apply all changes made"
+                );
+            }
+            assert_eq!(
+                lsp_request_count.load(Ordering::SeqCst),
+                3,
+                "Should query new hints one more time, for the last edit only"
+            );
+            let expected_hints = vec!["3".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last edit landed only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                2,
+                "Should update the cache version once more, for the new change"
+            );
+        });
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let _buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/main.rs", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let fake_server = fake_servers.next().await.unwrap();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
+        let lsp_request_count = Arc::new(AtomicUsize::new(0));
+        let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
+        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
+                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                    );
+
+                    task_lsp_request_ranges.lock().push(params.range);
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: params.range.end,
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        fn editor_visible_range(
+            editor: &ViewHandle<Editor>,
+            cx: &mut gpui::TestAppContext,
+        ) -> Range<Point> {
+            let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx));
+            assert_eq!(
+                ranges.len(),
+                1,
+                "Single buffer should produce a single excerpt with visible range"
+            );
+            let (_, (excerpt_buffer, _, excerpt_visible_range)) =
+                ranges.into_iter().next().unwrap();
+            excerpt_buffer.update(cx, |buffer, _| {
+                let snapshot = buffer.snapshot();
+                let start = buffer
+                    .anchor_before(excerpt_visible_range.start)
+                    .to_point(&snapshot);
+                let end = buffer
+                    .anchor_after(excerpt_visible_range.end)
+                    .to_point(&snapshot);
+                start..end
+            })
+        }
+
+        // in large buffers, requests are made for more than visible range of a buffer.
+        // invisible parts are queried later, to avoid excessive requests on quick typing.
+        // wait the timeout needed to get all requests.
+        cx.foreground().advance_clock(Duration::from_millis(
+            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
+        ));
+        cx.foreground().run_until_parked();
+        let initial_visible_range = editor_visible_range(&editor, cx);
+        let lsp_initial_visible_range = lsp::Range::new(
+            lsp::Position::new(
+                initial_visible_range.start.row,
+                initial_visible_range.start.column,
+            ),
+            lsp::Position::new(
+                initial_visible_range.end.row,
+                initial_visible_range.end.column,
+            ),
+        );
+        let expected_initial_query_range_end =
+            lsp::Position::new(initial_visible_range.end.row * 2, 2);
+        let mut expected_invisible_query_start = lsp_initial_visible_range.end;
+        expected_invisible_query_start.character += 1;
+        editor.update(cx, |editor, cx| {
+            let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            assert_eq!(ranges.len(), 2,
+                "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}");
+            let visible_query_range = &ranges[0];
+            assert_eq!(visible_query_range.start, lsp_initial_visible_range.start);
+            assert_eq!(visible_query_range.end, lsp_initial_visible_range.end);
+            let invisible_query_range = &ranges[1];
+
+            assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document");
+            assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document");
+
+            let requests_count = lsp_request_count.load(Ordering::Acquire);
+            assert_eq!(requests_count, 2, "Visible + invisible request");
+            let expected_hints = vec!["1".to_string(), "2".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should have hints from both LSP requests made for a big file"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
+            assert_eq!(
+                editor.inlay_hint_cache().version, requests_count,
+                "LSP queries should've bumped the cache version"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+            editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+        });
+        cx.foreground().advance_clock(Duration::from_millis(
+            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
+        ));
+        cx.foreground().run_until_parked();
+        let visible_range_after_scrolls = editor_visible_range(&editor, cx);
+        let visible_line_count =
+            editor.update(cx, |editor, _| editor.visible_line_count().unwrap());
+        let selection_in_cached_range = editor.update(cx, |editor, cx| {
+            let ranges = lsp_request_ranges
+                .lock()
+                .drain(..)
+                .sorted_by_key(|r| r.start)
+                .collect::<Vec<_>>();
+            assert_eq!(
+                ranges.len(),
+                2,
+                "Should query 2 ranges after both scrolls, but got: {ranges:?}"
+            );
+            let first_scroll = &ranges[0];
+            let second_scroll = &ranges[1];
+            assert_eq!(
+                first_scroll.end, second_scroll.start,
+                "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
+            );
+            assert_eq!(
+                first_scroll.start, expected_initial_query_range_end,
+                "First scroll should start the query right after the end of the original scroll",
+            );
+            assert_eq!(
+                second_scroll.end,
+                lsp::Position::new(
+                    visible_range_after_scrolls.end.row
+                        + visible_line_count.ceil() as u32,
+                    1,
+                ),
+                "Second scroll should query one more screen down after the end of the visible range"
+            );
+
+            let lsp_requests = lsp_request_count.load(Ordering::Acquire);
+            assert_eq!(lsp_requests, 4, "Should query for hints after every scroll");
+            let expected_hints = vec![
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string(),
+                "4".to_string(),
+            ];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should have hints from the new LSP response after the edit"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                lsp_requests,
+                "Should update the cache for every LSP response with hints added"
+            );
+
+            let mut selection_in_cached_range = visible_range_after_scrolls.end;
+            selection_in_cached_range.row -= visible_line_count.ceil() as u32;
+            selection_in_cached_range
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+                s.select_ranges([selection_in_cached_range..selection_in_cached_range])
+            });
+        });
+        cx.foreground().advance_clock(Duration::from_millis(
+            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
+        ));
+        cx.foreground().run_until_parked();
+        editor.update(cx, |_, _| {
+            let ranges = lsp_request_ranges
+                .lock()
+                .drain(..)
+                .sorted_by_key(|r| r.start)
+                .collect::<Vec<_>>();
+            assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
+            assert_eq!(lsp_request_count.load(Ordering::Acquire), 4);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.handle_input("++++more text++++", cx);
+        });
+        cx.foreground().advance_clock(Duration::from_millis(
+            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
+        ));
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            ranges.sort_by_key(|r| r.start);
+
+            assert_eq!(ranges.len(), 3,
+                "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
+            let above_query_range = &ranges[0];
+            let visible_query_range = &ranges[1];
+            let below_query_range = &ranges[2];
+            assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
+                "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
+            assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line  + 1 == below_query_range.start.line,
+                "Visible range {visible_query_range:?} should be before below range {below_query_range:?}");
+            assert!(above_query_range.start.line < selection_in_cached_range.row,
+                "Hints should be queried with the selected range after the query range start");
+            assert!(below_query_range.end.line > selection_in_cached_range.row,
+                "Hints should be queried with the selected range before the query range end");
+            assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32,
+                "Hints query range should contain one more screen before");
+            assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32,
+                "Hints query range should contain one more screen after");
+
+            let lsp_requests = lsp_request_count.load(Ordering::Acquire);
+            assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried");
+            let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()];
+            assert_eq!(expected_hints, cached_hint_labels(editor),
+                "Should have hints from the new LSP response after the edit");
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added");
+        });
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_multiple_excerpts_large_multibuffer(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let language = Arc::new(language);
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| {
+            project.languages().add(Arc::clone(&language))
+        });
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let buffer_1 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "main.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let buffer_2 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "other.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let multibuffer = cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(2, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(4, 0)..Point::new(11, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(22, 0)..Point::new(33, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(44, 0)..Point::new(55, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(56, 0)..Point::new(66, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(67, 0)..Point::new(77, 0),
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [
+                    ExcerptRange {
+                        context: Point::new(0, 1)..Point::new(2, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(4, 1)..Point::new(11, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(22, 1)..Point::new(33, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(44, 1)..Point::new(55, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(56, 1)..Point::new(66, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(67, 1)..Point::new(77, 1),
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer
+        });
+
+        deterministic.run_until_parked();
+        cx.foreground().run_until_parked();
+        let editor = cx
+            .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
+            .root(cx);
+        let editor_edited = Arc::new(AtomicBool::new(false));
+        let fake_server = fake_servers.next().await.unwrap();
+        let closure_editor_edited = Arc::clone(&editor_edited);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_editor_edited = Arc::clone(&closure_editor_edited);
+                async move {
+                    let hint_text = if params.text_document.uri
+                        == lsp::Url::from_file_path("/a/main.rs").unwrap()
+                    {
+                        "main hint"
+                    } else if params.text_document.uri
+                        == lsp::Url::from_file_path("/a/other.rs").unwrap()
+                    {
+                        "other hint"
+                    } else {
+                        panic!("unexpected uri: {:?}", params.text_document.uri);
+                    };
+
+                    // one hint per excerpt
+                    let positions = [
+                        lsp::Position::new(0, 2),
+                        lsp::Position::new(4, 2),
+                        lsp::Position::new(22, 2),
+                        lsp::Position::new(44, 2),
+                        lsp::Position::new(56, 2),
+                        lsp::Position::new(67, 2),
+                    ];
+                    let out_of_range_hint = lsp::InlayHint {
+                        position: lsp::Position::new(
+                            params.range.start.line + 99,
+                            params.range.start.character + 99,
+                        ),
+                        label: lsp::InlayHintLabel::String(
+                            "out of excerpt range, should be ignored".to_string(),
+                        ),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    };
+
+                    let edited = task_editor_edited.load(Ordering::Acquire);
+                    Ok(Some(
+                        std::iter::once(out_of_range_hint)
+                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
+                                lsp::InlayHint {
+                                    position,
+                                    label: lsp::InlayHintLabel::String(format!(
+                                        "{hint_text}{} #{i}",
+                                        if edited { "(edited)" } else { "" },
+                                    )),
+                                    kind: None,
+                                    text_edits: None,
+                                    tooltip: None,
+                                    padding_left: None,
+                                    padding_right: None,
+                                    data: None,
+                                }
+                            }))
+                            .collect(),
+                    ))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+            ];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison");
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+            });
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
+            });
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
+            });
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #4".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+            ];
+            assert_eq!(expected_hints, cached_hint_labels(editor),
+                "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
+                "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
+            });
+        });
+        cx.foreground().advance_clock(Duration::from_millis(
+            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
+        ));
+        cx.foreground().run_until_parked();
+        let last_scroll_update_version = editor.update(cx, |editor, cx| {
+            let expected_hints = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #4".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_hints, cached_hint_labels(editor),
+                "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
+            expected_hints.len()
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+            });
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #4".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_hints, cached_hint_labels(editor),
+                "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
+        });
+
+        editor_edited.store(true, Ordering::Release);
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
+            });
+            editor.handle_input("++++more text++++", cx);
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec![
+                "main hint(edited) #0".to_string(),
+                "main hint(edited) #1".to_string(),
+                "main hint(edited) #2".to_string(),
+                "main hint(edited) #3".to_string(),
+                "main hint(edited) #4".to_string(),
+                "main hint(edited) #5".to_string(),
+                "other hint(edited) #0".to_string(),
+                "other hint(edited) #1".to_string(),
+            ];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "After multibuffer edit, editor gets scolled back to the last selection; \
+all hints should be invalidated and requeried for all of its visible excerpts"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+
+            let current_cache_version = editor.inlay_hint_cache().version;
+            let minimum_expected_version = last_scroll_update_version + expected_hints.len();
+            assert!(
+                current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
+                "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_excerpts_removed(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: false,
+                show_parameter_hints: false,
+                show_other_hints: false,
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let language = Arc::new(language);
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| {
+            project.languages().add(Arc::clone(&language))
+        });
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let buffer_1 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "main.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let buffer_2 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "other.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+        let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
+            let buffer_1_excerpts = multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [ExcerptRange {
+                    context: Point::new(0, 0)..Point::new(2, 0),
+                    primary: None,
+                }],
+                cx,
+            );
+            let buffer_2_excerpts = multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [ExcerptRange {
+                    context: Point::new(0, 1)..Point::new(2, 1),
+                    primary: None,
+                }],
+                cx,
+            );
+            (buffer_1_excerpts, buffer_2_excerpts)
+        });
+
+        assert!(!buffer_1_excerpts.is_empty());
+        assert!(!buffer_2_excerpts.is_empty());
+
+        deterministic.run_until_parked();
+        cx.foreground().run_until_parked();
+        let editor = cx
+            .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
+            .root(cx);
+        let editor_edited = Arc::new(AtomicBool::new(false));
+        let fake_server = fake_servers.next().await.unwrap();
+        let closure_editor_edited = Arc::clone(&editor_edited);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_editor_edited = Arc::clone(&closure_editor_edited);
+                async move {
+                    let hint_text = if params.text_document.uri
+                        == lsp::Url::from_file_path("/a/main.rs").unwrap()
+                    {
+                        "main hint"
+                    } else if params.text_document.uri
+                        == lsp::Url::from_file_path("/a/other.rs").unwrap()
+                    {
+                        "other hint"
+                    } else {
+                        panic!("unexpected uri: {:?}", params.text_document.uri);
+                    };
+
+                    let positions = [
+                        lsp::Position::new(0, 2),
+                        lsp::Position::new(4, 2),
+                        lsp::Position::new(22, 2),
+                        lsp::Position::new(44, 2),
+                        lsp::Position::new(56, 2),
+                        lsp::Position::new(67, 2),
+                    ];
+                    let out_of_range_hint = lsp::InlayHint {
+                        position: lsp::Position::new(
+                            params.range.start.line + 99,
+                            params.range.start.character + 99,
+                        ),
+                        label: lsp::InlayHintLabel::String(
+                            "out of excerpt range, should be ignored".to_string(),
+                        ),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    };
+
+                    let edited = task_editor_edited.load(Ordering::Acquire);
+                    Ok(Some(
+                        std::iter::once(out_of_range_hint)
+                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
+                                lsp::InlayHint {
+                                    position,
+                                    label: lsp::InlayHintLabel::String(format!(
+                                        "{hint_text}{} #{i}",
+                                        if edited { "(edited)" } else { "" },
+                                    )),
+                                    kind: None,
+                                    text_edits: None,
+                                    tooltip: None,
+                                    padding_left: None,
+                                    padding_right: None,
+                                    data: None,
+                                }
+                            }))
+                            .collect(),
+                    ))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                vec!["main hint #0".to_string(), "other hint #0".to_string()],
+                cached_hint_labels(editor),
+                "Cache should update for both excerpts despite hints display was disabled"
+            );
+            assert!(
+                visible_hint_labels(editor, cx).is_empty(),
+                "All hints are disabled and should not be shown despite being present in the cache"
+            );
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                2,
+                "Cache should update once per excerpt query"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.buffer().update(cx, |multibuffer, cx| {
+                multibuffer.remove_excerpts(buffer_2_excerpts, cx)
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                vec!["main hint #0".to_string()],
+                cached_hint_labels(editor),
+                "For the removed excerpt, should clean corresponding cached hints"
+            );
+            assert!(
+                visible_hint_labels(editor, cx).is_empty(),
+                "All hints are disabled and should not be shown despite being present in the cache"
+            );
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                3,
+                "Excerpt removal should trigger a cache update"
+            );
+        });
+
+        update_test_language_settings(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["main hint #0".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Hint display settings change should not change the cache"
+            );
+            assert_eq!(
+                expected_hints,
+                visible_hint_labels(editor, cx),
+                "Settings change should make cached hints visible"
+            );
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                4,
+                "Settings change should trigger a cache update"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "โˆš".repeat(10)).repeat(500)),
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let _buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/main.rs", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let fake_server = fake_servers.next().await.unwrap();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                    );
+                    let query_start = params.range.start;
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: query_start,
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["1".to_string()];
+            assert_eq!(expected_hints, cached_hint_labels(editor));
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, 1);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: false,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+
+        editor.update(cx, |editor, cx| {
+            editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+        });
+        cx.foreground().start_waiting();
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["1".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should display inlays after toggle despite them disabled in settings"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(
+                editor.inlay_hint_cache().version,
+                1,
+                "First toggle should be cache's first update"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert!(
+                cached_hint_labels(editor).is_empty(),
+                "Should clear hints after 2nd toggle"
+            );
+            assert!(visible_hint_labels(editor, cx).is_empty());
+            assert_eq!(editor.inlay_hint_cache().version, 2);
+        });
+
+        update_test_language_settings(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["2".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should query LSP hints for the 2nd time after enabling hints in settings"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, 3);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert!(
+                cached_hint_labels(editor).is_empty(),
+                "Should clear hints after enabling in settings and a 3rd toggle"
+            );
+            assert!(visible_hint_labels(editor, cx).is_empty());
+            assert_eq!(editor.inlay_hint_cache().version, 4);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_hints = vec!["3".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            assert_eq!(editor.inlay_hint_cache().version, 5);
+        });
+    }
+
+    pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+        cx.foreground().forbid_parking();
+
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            client::init_settings(cx);
+            language::init(cx);
+            Project::init_settings(cx);
+            workspace::init_settings(cx);
+            crate::init(cx);
+        });
+
+        update_test_language_settings(cx, f);
+    }
+
+    async fn prepare_test_objects(
+        cx: &mut TestAppContext,
+    ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let _buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/main.rs", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let fake_server = fake_servers.next().await.unwrap();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        editor.update(cx, |editor, cx| {
+            assert!(cached_hint_labels(editor).is_empty());
+            assert!(visible_hint_labels(editor, cx).is_empty());
+            assert_eq!(editor.inlay_hint_cache().version, 0);
+        });
+
+        ("/a/main.rs", editor, fake_server)
+    }
+
+    pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
+        let mut labels = Vec::new();
+        for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+            let excerpt_hints = excerpt_hints.read();
+            for id in &excerpt_hints.ordered_hints {
+                labels.push(excerpt_hints.hints_by_id[id].text());
+            }
+        }
+
+        labels.sort();
+        labels
+    }
+
+    pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
+        let mut hints = editor
+            .visible_inlay_hints(cx)
+            .into_iter()
+            .map(|hint| hint.text.to_string())
+            .collect::<Vec<_>>();
+        hints.sort();
+        hints
+    }
+}

crates/editor2/src/items.rs ๐Ÿ”—

@@ -0,0 +1,1327 @@
+use crate::{
+    display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
+    movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
+    Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
+};
+use anyhow::{Context, Result};
+use collections::HashSet;
+use futures::future::try_join_all;
+use gpui::{
+    elements::*,
+    geometry::vector::{vec2f, Vector2F},
+    AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext,
+    ViewHandle, WeakViewHandle,
+};
+use language::{
+    proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
+    SelectionGoal,
+};
+use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
+use rpc::proto::{self, update_view, PeerId};
+use smallvec::SmallVec;
+use std::{
+    borrow::Cow,
+    cmp::{self, Ordering},
+    fmt::Write,
+    iter,
+    ops::Range,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use text::Selection;
+use util::{
+    paths::{PathExt, FILE_ROW_COLUMN_DELIMITER},
+    ResultExt, TryFutureExt,
+};
+use workspace::item::{BreadcrumbText, FollowableItemHandle};
+use workspace::{
+    item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
+    searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
+    ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace,
+    WorkspaceId,
+};
+
+pub const MAX_TAB_TITLE_LEN: usize = 24;
+
+impl FollowableItem for Editor {
+    fn remote_id(&self) -> Option<ViewId> {
+        self.remote_id
+    }
+
+    fn from_state_proto(
+        pane: ViewHandle<workspace::Pane>,
+        workspace: ViewHandle<Workspace>,
+        remote_id: ViewId,
+        state: &mut Option<proto::view::Variant>,
+        cx: &mut AppContext,
+    ) -> Option<Task<Result<ViewHandle<Self>>>> {
+        let project = workspace.read(cx).project().to_owned();
+        let Some(proto::view::Variant::Editor(_)) = state else {
+            return None;
+        };
+        let Some(proto::view::Variant::Editor(state)) = state.take() else {
+            unreachable!()
+        };
+
+        let client = project.read(cx).client();
+        let replica_id = project.read(cx).replica_id();
+        let buffer_ids = state
+            .excerpts
+            .iter()
+            .map(|excerpt| excerpt.buffer_id)
+            .collect::<HashSet<_>>();
+        let buffers = project.update(cx, |project, cx| {
+            buffer_ids
+                .iter()
+                .map(|id| project.open_buffer_by_id(*id, cx))
+                .collect::<Vec<_>>()
+        });
+
+        let pane = pane.downgrade();
+        Some(cx.spawn(|mut cx| async move {
+            let mut buffers = futures::future::try_join_all(buffers).await?;
+            let editor = pane.read_with(&cx, |pane, cx| {
+                let mut editors = pane.items_of_type::<Self>();
+                editors.find(|editor| {
+                    let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
+                    let singleton_buffer_matches = state.singleton
+                        && buffers.first()
+                            == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
+                    ids_match || singleton_buffer_matches
+                })
+            })?;
+
+            let editor = if let Some(editor) = editor {
+                editor
+            } else {
+                pane.update(&mut cx, |_, cx| {
+                    let multibuffer = cx.add_model(|cx| {
+                        let mut multibuffer;
+                        if state.singleton && buffers.len() == 1 {
+                            multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
+                        } else {
+                            multibuffer = MultiBuffer::new(replica_id);
+                            let mut excerpts = state.excerpts.into_iter().peekable();
+                            while let Some(excerpt) = excerpts.peek() {
+                                let buffer_id = excerpt.buffer_id;
+                                let buffer_excerpts = iter::from_fn(|| {
+                                    let excerpt = excerpts.peek()?;
+                                    (excerpt.buffer_id == buffer_id)
+                                        .then(|| excerpts.next().unwrap())
+                                });
+                                let buffer =
+                                    buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
+                                if let Some(buffer) = buffer {
+                                    multibuffer.push_excerpts(
+                                        buffer.clone(),
+                                        buffer_excerpts.filter_map(deserialize_excerpt_range),
+                                        cx,
+                                    );
+                                }
+                            }
+                        };
+
+                        if let Some(title) = &state.title {
+                            multibuffer = multibuffer.with_title(title.clone())
+                        }
+
+                        multibuffer
+                    });
+
+                    cx.add_view(|cx| {
+                        let mut editor =
+                            Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
+                        editor.remote_id = Some(remote_id);
+                        editor
+                    })
+                })?
+            };
+
+            update_editor_from_message(
+                editor.downgrade(),
+                project,
+                proto::update_view::Editor {
+                    selections: state.selections,
+                    pending_selection: state.pending_selection,
+                    scroll_top_anchor: state.scroll_top_anchor,
+                    scroll_x: state.scroll_x,
+                    scroll_y: state.scroll_y,
+                    ..Default::default()
+                },
+                &mut cx,
+            )
+            .await?;
+
+            Ok(editor)
+        }))
+    }
+
+    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
+        self.leader_peer_id = leader_peer_id;
+        if self.leader_peer_id.is_some() {
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.remove_active_selections(cx);
+            });
+        } else {
+            self.buffer.update(cx, |buffer, cx| {
+                if self.focused {
+                    buffer.set_active_selections(
+                        &self.selections.disjoint_anchors(),
+                        self.selections.line_mode,
+                        self.cursor_shape,
+                        cx,
+                    );
+                }
+            });
+        }
+        cx.notify();
+    }
+
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+        let buffer = self.buffer.read(cx);
+        let scroll_anchor = self.scroll_manager.anchor();
+        let excerpts = buffer
+            .read(cx)
+            .excerpts()
+            .map(|(id, buffer, range)| proto::Excerpt {
+                id: id.to_proto(),
+                buffer_id: buffer.remote_id(),
+                context_start: Some(serialize_text_anchor(&range.context.start)),
+                context_end: Some(serialize_text_anchor(&range.context.end)),
+                primary_start: range
+                    .primary
+                    .as_ref()
+                    .map(|range| serialize_text_anchor(&range.start)),
+                primary_end: range
+                    .primary
+                    .as_ref()
+                    .map(|range| serialize_text_anchor(&range.end)),
+            })
+            .collect();
+
+        Some(proto::view::Variant::Editor(proto::view::Editor {
+            singleton: buffer.is_singleton(),
+            title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
+            excerpts,
+            scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
+            scroll_x: scroll_anchor.offset.x(),
+            scroll_y: scroll_anchor.offset.y(),
+            selections: self
+                .selections
+                .disjoint_anchors()
+                .iter()
+                .map(serialize_selection)
+                .collect(),
+            pending_selection: self
+                .selections
+                .pending_anchor()
+                .as_ref()
+                .map(serialize_selection),
+        }))
+    }
+
+    fn add_event_to_update_proto(
+        &self,
+        event: &Self::Event,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &AppContext,
+    ) -> bool {
+        let update =
+            update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
+
+        match update {
+            proto::update_view::Variant::Editor(update) => match event {
+                Event::ExcerptsAdded {
+                    buffer,
+                    predecessor,
+                    excerpts,
+                } => {
+                    let buffer_id = buffer.read(cx).remote_id();
+                    let mut excerpts = excerpts.iter();
+                    if let Some((id, range)) = excerpts.next() {
+                        update.inserted_excerpts.push(proto::ExcerptInsertion {
+                            previous_excerpt_id: Some(predecessor.to_proto()),
+                            excerpt: serialize_excerpt(buffer_id, id, range),
+                        });
+                        update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
+                            proto::ExcerptInsertion {
+                                previous_excerpt_id: None,
+                                excerpt: serialize_excerpt(buffer_id, id, range),
+                            }
+                        }))
+                    }
+                    true
+                }
+                Event::ExcerptsRemoved { ids } => {
+                    update
+                        .deleted_excerpts
+                        .extend(ids.iter().map(ExcerptId::to_proto));
+                    true
+                }
+                Event::ScrollPositionChanged { .. } => {
+                    let scroll_anchor = self.scroll_manager.anchor();
+                    update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
+                    update.scroll_x = scroll_anchor.offset.x();
+                    update.scroll_y = scroll_anchor.offset.y();
+                    true
+                }
+                Event::SelectionsChanged { .. } => {
+                    update.selections = self
+                        .selections
+                        .disjoint_anchors()
+                        .iter()
+                        .map(serialize_selection)
+                        .collect();
+                    update.pending_selection = self
+                        .selections
+                        .pending_anchor()
+                        .as_ref()
+                        .map(serialize_selection);
+                    true
+                }
+                _ => false,
+            },
+        }
+    }
+
+    fn apply_update_proto(
+        &mut self,
+        project: &ModelHandle<Project>,
+        message: update_view::Variant,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let update_view::Variant::Editor(message) = message;
+        let project = project.clone();
+        cx.spawn(|this, mut cx| async move {
+            update_editor_from_message(this, project, message, &mut cx).await
+        })
+    }
+
+    fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
+        match event {
+            Event::Edited => true,
+            Event::SelectionsChanged { local } => *local,
+            Event::ScrollPositionChanged { local, .. } => *local,
+            _ => false,
+        }
+    }
+
+    fn is_project_item(&self, _cx: &AppContext) -> bool {
+        true
+    }
+}
+
+async fn update_editor_from_message(
+    this: WeakViewHandle<Editor>,
+    project: ModelHandle<Project>,
+    message: proto::update_view::Editor,
+    cx: &mut AsyncAppContext,
+) -> Result<()> {
+    // Open all of the buffers of which excerpts were added to the editor.
+    let inserted_excerpt_buffer_ids = message
+        .inserted_excerpts
+        .iter()
+        .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
+        .collect::<HashSet<_>>();
+    let inserted_excerpt_buffers = project.update(cx, |project, cx| {
+        inserted_excerpt_buffer_ids
+            .into_iter()
+            .map(|id| project.open_buffer_by_id(id, cx))
+            .collect::<Vec<_>>()
+    });
+    let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
+
+    // Update the editor's excerpts.
+    this.update(cx, |editor, cx| {
+        editor.buffer.update(cx, |multibuffer, cx| {
+            let mut removed_excerpt_ids = message
+                .deleted_excerpts
+                .into_iter()
+                .map(ExcerptId::from_proto)
+                .collect::<Vec<_>>();
+            removed_excerpt_ids.sort_by({
+                let multibuffer = multibuffer.read(cx);
+                move |a, b| a.cmp(&b, &multibuffer)
+            });
+
+            let mut insertions = message.inserted_excerpts.into_iter().peekable();
+            while let Some(insertion) = insertions.next() {
+                let Some(excerpt) = insertion.excerpt else {
+                    continue;
+                };
+                let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
+                    continue;
+                };
+                let buffer_id = excerpt.buffer_id;
+                let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
+                    continue;
+                };
+
+                let adjacent_excerpts = iter::from_fn(|| {
+                    let insertion = insertions.peek()?;
+                    if insertion.previous_excerpt_id.is_none()
+                        && insertion.excerpt.as_ref()?.buffer_id == buffer_id
+                    {
+                        insertions.next()?.excerpt
+                    } else {
+                        None
+                    }
+                });
+
+                multibuffer.insert_excerpts_with_ids_after(
+                    ExcerptId::from_proto(previous_excerpt_id),
+                    buffer,
+                    [excerpt]
+                        .into_iter()
+                        .chain(adjacent_excerpts)
+                        .filter_map(|excerpt| {
+                            Some((
+                                ExcerptId::from_proto(excerpt.id),
+                                deserialize_excerpt_range(excerpt)?,
+                            ))
+                        }),
+                    cx,
+                );
+            }
+
+            multibuffer.remove_excerpts(removed_excerpt_ids, cx);
+        });
+    })?;
+
+    // Deserialize the editor state.
+    let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
+        let buffer = editor.buffer.read(cx).read(cx);
+        let selections = message
+            .selections
+            .into_iter()
+            .filter_map(|selection| deserialize_selection(&buffer, selection))
+            .collect::<Vec<_>>();
+        let pending_selection = message
+            .pending_selection
+            .and_then(|selection| deserialize_selection(&buffer, selection));
+        let scroll_top_anchor = message
+            .scroll_top_anchor
+            .and_then(|anchor| deserialize_anchor(&buffer, anchor));
+        anyhow::Ok((selections, pending_selection, scroll_top_anchor))
+    })??;
+
+    // Wait until the buffer has received all of the operations referenced by
+    // the editor's new state.
+    this.update(cx, |editor, cx| {
+        editor.buffer.update(cx, |buffer, cx| {
+            buffer.wait_for_anchors(
+                selections
+                    .iter()
+                    .chain(pending_selection.as_ref())
+                    .flat_map(|selection| [selection.start, selection.end])
+                    .chain(scroll_top_anchor),
+                cx,
+            )
+        })
+    })?
+    .await?;
+
+    // Update the editor's state.
+    this.update(cx, |editor, cx| {
+        if !selections.is_empty() || pending_selection.is_some() {
+            editor.set_selections_from_remote(selections, pending_selection, cx);
+            editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
+        } else if let Some(scroll_top_anchor) = scroll_top_anchor {
+            editor.set_scroll_anchor_remote(
+                ScrollAnchor {
+                    anchor: scroll_top_anchor,
+                    offset: vec2f(message.scroll_x, message.scroll_y),
+                },
+                cx,
+            );
+        }
+    })?;
+    Ok(())
+}
+
+fn serialize_excerpt(
+    buffer_id: u64,
+    id: &ExcerptId,
+    range: &ExcerptRange<language::Anchor>,
+) -> Option<proto::Excerpt> {
+    Some(proto::Excerpt {
+        id: id.to_proto(),
+        buffer_id,
+        context_start: Some(serialize_text_anchor(&range.context.start)),
+        context_end: Some(serialize_text_anchor(&range.context.end)),
+        primary_start: range
+            .primary
+            .as_ref()
+            .map(|r| serialize_text_anchor(&r.start)),
+        primary_end: range
+            .primary
+            .as_ref()
+            .map(|r| serialize_text_anchor(&r.end)),
+    })
+}
+
+fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
+    proto::Selection {
+        id: selection.id as u64,
+        start: Some(serialize_anchor(&selection.start)),
+        end: Some(serialize_anchor(&selection.end)),
+        reversed: selection.reversed,
+    }
+}
+
+fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor {
+    proto::EditorAnchor {
+        excerpt_id: anchor.excerpt_id.to_proto(),
+        anchor: Some(serialize_text_anchor(&anchor.text_anchor)),
+    }
+}
+
+fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option<ExcerptRange<language::Anchor>> {
+    let context = {
+        let start = language::proto::deserialize_anchor(excerpt.context_start?)?;
+        let end = language::proto::deserialize_anchor(excerpt.context_end?)?;
+        start..end
+    };
+    let primary = excerpt
+        .primary_start
+        .zip(excerpt.primary_end)
+        .and_then(|(start, end)| {
+            let start = language::proto::deserialize_anchor(start)?;
+            let end = language::proto::deserialize_anchor(end)?;
+            Some(start..end)
+        });
+    Some(ExcerptRange { context, primary })
+}
+
+fn deserialize_selection(
+    buffer: &MultiBufferSnapshot,
+    selection: proto::Selection,
+) -> Option<Selection<Anchor>> {
+    Some(Selection {
+        id: selection.id as usize,
+        start: deserialize_anchor(buffer, selection.start?)?,
+        end: deserialize_anchor(buffer, selection.end?)?,
+        reversed: selection.reversed,
+        goal: SelectionGoal::None,
+    })
+}
+
+fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option<Anchor> {
+    let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id);
+    Some(Anchor {
+        excerpt_id,
+        text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
+        buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
+    })
+}
+
+impl Item for Editor {
+    fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
+        if let Ok(data) = data.downcast::<NavigationData>() {
+            let newest_selection = self.selections.newest::<Point>(cx);
+            let buffer = self.buffer.read(cx).read(cx);
+            let offset = if buffer.can_resolve(&data.cursor_anchor) {
+                data.cursor_anchor.to_point(&buffer)
+            } else {
+                buffer.clip_point(data.cursor_position, Bias::Left)
+            };
+
+            let mut scroll_anchor = data.scroll_anchor;
+            if !buffer.can_resolve(&scroll_anchor.anchor) {
+                scroll_anchor.anchor = buffer.anchor_before(
+                    buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
+                );
+            }
+
+            drop(buffer);
+
+            if newest_selection.head() == offset {
+                false
+            } else {
+                let nav_history = self.nav_history.take();
+                self.set_scroll_anchor(scroll_anchor, cx);
+                self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.select_ranges([offset..offset])
+                });
+                self.nav_history = nav_history;
+                true
+            }
+        } else {
+            false
+        }
+    }
+
+    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
+        let file_path = self
+            .buffer()
+            .read(cx)
+            .as_singleton()?
+            .read(cx)
+            .file()
+            .and_then(|f| f.as_local())?
+            .abs_path(cx);
+
+        let file_path = file_path.compact().to_string_lossy().to_string();
+
+        Some(file_path.into())
+    }
+
+    fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<str>> {
+        match path_for_buffer(&self.buffer, detail, true, cx)? {
+            Cow::Borrowed(path) => Some(path.to_string_lossy()),
+            Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
+        }
+    }
+
+    fn tab_content<T: 'static>(
+        &self,
+        detail: Option<usize>,
+        style: &theme::Tab,
+        cx: &AppContext,
+    ) -> AnyElement<T> {
+        Flex::row()
+            .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any())
+            .with_children(detail.and_then(|detail| {
+                let path = path_for_buffer(&self.buffer, detail, false, cx)?;
+                let description = path.to_string_lossy();
+                Some(
+                    Label::new(
+                        util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN),
+                        style.description.text.clone(),
+                    )
+                    .contained()
+                    .with_style(style.description.container)
+                    .aligned(),
+                )
+            }))
+            .align_children_center()
+            .into_any()
+    }
+
+    fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
+        self.buffer
+            .read(cx)
+            .for_each_buffer(|buffer| f(buffer.id(), buffer.read(cx)));
+    }
+
+    fn is_singleton(&self, cx: &AppContext) -> bool {
+        self.buffer.read(cx).is_singleton()
+    }
+
+    fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
+    where
+        Self: Sized,
+    {
+        Some(self.clone(cx))
+    }
+
+    fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+        self.nav_history = Some(history);
+    }
+
+    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+        let selection = self.selections.newest_anchor();
+        self.push_to_nav_history(selection.head(), None, cx);
+    }
+
+    fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
+        hide_link_definition(self, cx);
+        self.link_go_to_definition_state.last_trigger_point = None;
+    }
+
+    fn is_dirty(&self, cx: &AppContext) -> bool {
+        self.buffer().read(cx).read(cx).is_dirty()
+    }
+
+    fn has_conflict(&self, cx: &AppContext) -> bool {
+        self.buffer().read(cx).read(cx).has_conflict()
+    }
+
+    fn can_save(&self, cx: &AppContext) -> bool {
+        let buffer = &self.buffer().read(cx);
+        if let Some(buffer) = buffer.as_singleton() {
+            buffer.read(cx).project_path(cx).is_some()
+        } else {
+            true
+        }
+    }
+
+    fn save(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        self.report_editor_event("save", None, cx);
+        let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
+        let buffers = self.buffer().clone().read(cx).all_buffers();
+        cx.spawn(|_, mut cx| async move {
+            format.await?;
+
+            if buffers.len() == 1 {
+                project
+                    .update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
+                    .await?;
+            } else {
+                // For multi-buffers, only save those ones that contain changes. For clean buffers
+                // we simulate saving by calling `Buffer::did_save`, so that language servers or
+                // other downstream listeners of save events get notified.
+                let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
+                    buffer.read_with(&cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict())
+                });
+
+                project
+                    .update(&mut cx, |project, cx| {
+                        project.save_buffers(dirty_buffers, cx)
+                    })
+                    .await?;
+                for buffer in clean_buffers {
+                    buffer.update(&mut cx, |buffer, cx| {
+                        let version = buffer.saved_version().clone();
+                        let fingerprint = buffer.saved_version_fingerprint();
+                        let mtime = buffer.saved_mtime();
+                        buffer.did_save(version, fingerprint, mtime, cx);
+                    });
+                }
+            }
+
+            Ok(())
+        })
+    }
+
+    fn save_as(
+        &mut self,
+        project: ModelHandle<Project>,
+        abs_path: PathBuf,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let buffer = self
+            .buffer()
+            .read(cx)
+            .as_singleton()
+            .expect("cannot call save_as on an excerpt list");
+
+        let file_extension = abs_path
+            .extension()
+            .map(|a| a.to_string_lossy().to_string());
+        self.report_editor_event("save", file_extension, cx);
+
+        project.update(cx, |project, cx| {
+            project.save_buffer_as(buffer, abs_path, cx)
+        })
+    }
+
+    fn reload(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let buffer = self.buffer().clone();
+        let buffers = self.buffer.read(cx).all_buffers();
+        let reload_buffers =
+            project.update(cx, |project, cx| project.reload_buffers(buffers, true, cx));
+        cx.spawn(|this, mut cx| async move {
+            let transaction = reload_buffers.log_err().await;
+            this.update(&mut cx, |editor, cx| {
+                editor.request_autoscroll(Autoscroll::fit(), cx)
+            })?;
+            buffer.update(&mut cx, |buffer, cx| {
+                if let Some(transaction) = transaction {
+                    if !buffer.is_singleton() {
+                        buffer.push_transaction(&transaction.0, cx);
+                    }
+                }
+            });
+            Ok(())
+        })
+    }
+
+    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+        let mut result = SmallVec::new();
+        match event {
+            Event::Closed => result.push(ItemEvent::CloseItem),
+            Event::Saved | Event::TitleChanged => {
+                result.push(ItemEvent::UpdateTab);
+                result.push(ItemEvent::UpdateBreadcrumbs);
+            }
+            Event::Reparsed => {
+                result.push(ItemEvent::UpdateBreadcrumbs);
+            }
+            Event::SelectionsChanged { local } if *local => {
+                result.push(ItemEvent::UpdateBreadcrumbs);
+            }
+            Event::DirtyChanged => {
+                result.push(ItemEvent::UpdateTab);
+            }
+            Event::BufferEdited => {
+                result.push(ItemEvent::Edit);
+                result.push(ItemEvent::UpdateBreadcrumbs);
+            }
+            _ => {}
+        }
+        result
+    }
+
+    fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+        Some(Box::new(handle.clone()))
+    }
+
+    fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
+        self.pixel_position_of_newest_cursor
+    }
+
+    fn breadcrumb_location(&self) -> ToolbarItemLocation {
+        ToolbarItemLocation::PrimaryLeft { flex: None }
+    }
+
+    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
+        let cursor = self.selections.newest_anchor().head();
+        let multibuffer = &self.buffer().read(cx);
+        let (buffer_id, symbols) =
+            multibuffer.symbols_containing(cursor, Some(&theme.editor.syntax), cx)?;
+        let buffer = multibuffer.buffer(buffer_id)?;
+
+        let buffer = buffer.read(cx);
+        let filename = buffer
+            .snapshot()
+            .resolve_file_path(
+                cx,
+                self.project
+                    .as_ref()
+                    .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+                    .unwrap_or_default(),
+            )
+            .map(|path| path.to_string_lossy().to_string())
+            .unwrap_or_else(|| "untitled".to_string());
+
+        let mut breadcrumbs = vec![BreadcrumbText {
+            text: filename,
+            highlights: None,
+        }];
+        breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
+            text: symbol.text,
+            highlights: Some(symbol.highlight_ranges),
+        }));
+        Some(breadcrumbs)
+    }
+
+    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+        let workspace_id = workspace.database_id();
+        let item_id = cx.view_id();
+        self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
+
+        fn serialize(
+            buffer: ModelHandle<Buffer>,
+            workspace_id: WorkspaceId,
+            item_id: ItemId,
+            cx: &mut AppContext,
+        ) {
+            if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
+                let path = file.abs_path(cx);
+
+                cx.background()
+                    .spawn(async move {
+                        DB.save_path(item_id, workspace_id, path.clone())
+                            .await
+                            .log_err()
+                    })
+                    .detach();
+            }
+        }
+
+        if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+            serialize(buffer.clone(), workspace_id, item_id, cx);
+
+            cx.subscribe(&buffer, |this, buffer, event, cx| {
+                if let Some((_, workspace_id)) = this.workspace.as_ref() {
+                    if let language::Event::FileHandleChanged = event {
+                        serialize(buffer, *workspace_id, cx.view_id(), cx);
+                    }
+                }
+            })
+            .detach();
+        }
+    }
+
+    fn serialized_item_kind() -> Option<&'static str> {
+        Some("Editor")
+    }
+
+    fn deserialize(
+        project: ModelHandle<Project>,
+        _workspace: WeakViewHandle<Workspace>,
+        workspace_id: workspace::WorkspaceId,
+        item_id: ItemId,
+        cx: &mut ViewContext<Pane>,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        let project_item: Result<_> = project.update(cx, |project, cx| {
+            // Look up the path with this key associated, create a self with that path
+            let path = DB
+                .get_path(item_id, workspace_id)?
+                .context("No path stored for this editor")?;
+
+            let (worktree, path) = project
+                .find_local_worktree(&path, cx)
+                .with_context(|| format!("No worktree for path: {path:?}"))?;
+            let project_path = ProjectPath {
+                worktree_id: worktree.read(cx).id(),
+                path: path.into(),
+            };
+
+            Ok(project.open_path(project_path, cx))
+        });
+
+        project_item
+            .map(|project_item| {
+                cx.spawn(|pane, mut cx| async move {
+                    let (_, project_item) = project_item.await?;
+                    let buffer = project_item
+                        .downcast::<Buffer>()
+                        .context("Project item at stored path was not a buffer")?;
+                    Ok(pane.update(&mut cx, |_, cx| {
+                        cx.add_view(|cx| {
+                            let mut editor = Editor::for_buffer(buffer, Some(project), cx);
+                            editor.read_scroll_position_from_db(item_id, workspace_id, cx);
+                            editor
+                        })
+                    })?)
+                })
+            })
+            .unwrap_or_else(|error| Task::ready(Err(error)))
+    }
+}
+
+impl ProjectItem for Editor {
+    type Item = Buffer;
+
+    fn for_project_item(
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        Self::for_buffer(buffer, Some(project), cx)
+    }
+}
+
+pub(crate) enum BufferSearchHighlights {}
+impl SearchableItem for Editor {
+    type Match = Range<Anchor>;
+
+    fn to_search_event(
+        &mut self,
+        event: &Self::Event,
+        _: &mut ViewContext<Self>,
+    ) -> Option<SearchEvent> {
+        match event {
+            Event::BufferEdited => Some(SearchEvent::MatchesInvalidated),
+            Event::SelectionsChanged { .. } => {
+                if self.selections.disjoint_anchors().len() == 1 {
+                    Some(SearchEvent::ActiveMatchChanged)
+                } else {
+                    None
+                }
+            }
+            _ => None,
+        }
+    }
+
+    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
+        self.clear_background_highlights::<BufferSearchHighlights>(cx);
+    }
+
+    fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
+        self.highlight_background::<BufferSearchHighlights>(
+            matches,
+            |theme| theme.search.match_background,
+            cx,
+        );
+    }
+
+    fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
+        let display_map = self.snapshot(cx).display_snapshot;
+        let selection = self.selections.newest::<usize>(cx);
+        if selection.start == selection.end {
+            let point = selection.start.to_display_point(&display_map);
+            let range = surrounding_word(&display_map, point);
+            let range = range.start.to_offset(&display_map, Bias::Left)
+                ..range.end.to_offset(&display_map, Bias::Right);
+            let text: String = display_map.buffer_snapshot.text_for_range(range).collect();
+            if text.trim().is_empty() {
+                String::new()
+            } else {
+                text
+            }
+        } else {
+            display_map
+                .buffer_snapshot
+                .text_for_range(selection.start..selection.end)
+                .collect()
+        }
+    }
+
+    fn activate_match(
+        &mut self,
+        index: usize,
+        matches: Vec<Range<Anchor>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.unfold_ranges([matches[index].clone()], false, true, cx);
+        let range = self.range_for_match(&matches[index]);
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.select_ranges([range]);
+        })
+    }
+
+    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        self.unfold_ranges(matches.clone(), false, false, cx);
+        let mut ranges = Vec::new();
+        for m in &matches {
+            ranges.push(self.range_for_match(&m))
+        }
+        self.change_selections(None, cx, |s| s.select_ranges(ranges));
+    }
+    fn replace(
+        &mut self,
+        identifier: &Self::Match,
+        query: &SearchQuery,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let text = self.buffer.read(cx);
+        let text = text.snapshot(cx);
+        let text = text.text_for_range(identifier.clone()).collect::<Vec<_>>();
+        let text: Cow<_> = if text.len() == 1 {
+            text.first().cloned().unwrap().into()
+        } else {
+            let joined_chunks = text.join("");
+            joined_chunks.into()
+        };
+
+        if let Some(replacement) = query.replacement_for(&text) {
+            self.transact(cx, |this, cx| {
+                this.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
+            });
+        }
+    }
+    fn match_index_for_direction(
+        &mut self,
+        matches: &Vec<Range<Anchor>>,
+        current_index: usize,
+        direction: Direction,
+        count: usize,
+        cx: &mut ViewContext<Self>,
+    ) -> usize {
+        let buffer = self.buffer().read(cx).snapshot(cx);
+        let current_index_position = if self.selections.disjoint_anchors().len() == 1 {
+            self.selections.newest_anchor().head()
+        } else {
+            matches[current_index].start
+        };
+
+        let mut count = count % matches.len();
+        if count == 0 {
+            return current_index;
+        }
+        match direction {
+            Direction::Next => {
+                if matches[current_index]
+                    .start
+                    .cmp(&current_index_position, &buffer)
+                    .is_gt()
+                {
+                    count = count - 1
+                }
+
+                (current_index + count) % matches.len()
+            }
+            Direction::Prev => {
+                if matches[current_index]
+                    .end
+                    .cmp(&current_index_position, &buffer)
+                    .is_lt()
+                {
+                    count = count - 1;
+                }
+
+                if current_index >= count {
+                    current_index - count
+                } else {
+                    matches.len() - (count - current_index)
+                }
+            }
+        }
+    }
+
+    fn find_matches(
+        &mut self,
+        query: Arc<project::search::SearchQuery>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Vec<Range<Anchor>>> {
+        let buffer = self.buffer().read(cx).snapshot(cx);
+        cx.background().spawn(async move {
+            let mut ranges = Vec::new();
+            if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
+                ranges.extend(
+                    query
+                        .search(excerpt_buffer, None)
+                        .await
+                        .into_iter()
+                        .map(|range| {
+                            buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
+                        }),
+                );
+            } else {
+                for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
+                    let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
+                    ranges.extend(
+                        query
+                            .search(&excerpt.buffer, Some(excerpt_range.clone()))
+                            .await
+                            .into_iter()
+                            .map(|range| {
+                                let start = excerpt
+                                    .buffer
+                                    .anchor_after(excerpt_range.start + range.start);
+                                let end = excerpt
+                                    .buffer
+                                    .anchor_before(excerpt_range.start + range.end);
+                                buffer.anchor_in_excerpt(excerpt.id.clone(), start)
+                                    ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
+                            }),
+                    );
+                }
+            }
+            ranges
+        })
+    }
+
+    fn active_match_index(
+        &mut self,
+        matches: Vec<Range<Anchor>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<usize> {
+        active_match_index(
+            &matches,
+            &self.selections.newest_anchor().head(),
+            &self.buffer().read(cx).snapshot(cx),
+        )
+    }
+}
+
+pub fn active_match_index(
+    ranges: &[Range<Anchor>],
+    cursor: &Anchor,
+    buffer: &MultiBufferSnapshot,
+) -> Option<usize> {
+    if ranges.is_empty() {
+        None
+    } else {
+        match ranges.binary_search_by(|probe| {
+            if probe.end.cmp(cursor, &*buffer).is_lt() {
+                Ordering::Less
+            } else if probe.start.cmp(cursor, &*buffer).is_gt() {
+                Ordering::Greater
+            } else {
+                Ordering::Equal
+            }
+        }) {
+            Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
+        }
+    }
+}
+
+pub struct CursorPosition {
+    position: Option<Point>,
+    selected_count: usize,
+    _observe_active_editor: Option<Subscription>,
+}
+
+impl Default for CursorPosition {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl CursorPosition {
+    pub fn new() -> Self {
+        Self {
+            position: None,
+            selected_count: 0,
+            _observe_active_editor: None,
+        }
+    }
+
+    fn update_position(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+        let editor = editor.read(cx);
+        let buffer = editor.buffer().read(cx).snapshot(cx);
+
+        self.selected_count = 0;
+        let mut last_selection: Option<Selection<usize>> = None;
+        for selection in editor.selections.all::<usize>(cx) {
+            self.selected_count += selection.end - selection.start;
+            if last_selection
+                .as_ref()
+                .map_or(true, |last_selection| selection.id > last_selection.id)
+            {
+                last_selection = Some(selection);
+            }
+        }
+        self.position = last_selection.map(|s| s.head().to_point(&buffer));
+
+        cx.notify();
+    }
+}
+
+impl Entity for CursorPosition {
+    type Event = ();
+}
+
+impl View for CursorPosition {
+    fn ui_name() -> &'static str {
+        "CursorPosition"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        if let Some(position) = self.position {
+            let theme = &theme::current(cx).workspace.status_bar;
+            let mut text = format!(
+                "{}{FILE_ROW_COLUMN_DELIMITER}{}",
+                position.row + 1,
+                position.column + 1
+            );
+            if self.selected_count > 0 {
+                write!(text, " ({} selected)", self.selected_count).unwrap();
+            }
+            Label::new(text, theme.cursor_position.clone()).into_any()
+        } else {
+            Empty::new().into_any()
+        }
+    }
+}
+
+impl StatusItemView for CursorPosition {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
+            self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
+            self.update_position(editor, cx);
+        } else {
+            self.position = None;
+            self._observe_active_editor = None;
+        }
+
+        cx.notify();
+    }
+}
+
+fn path_for_buffer<'a>(
+    buffer: &ModelHandle<MultiBuffer>,
+    height: usize,
+    include_filename: bool,
+    cx: &'a AppContext,
+) -> Option<Cow<'a, Path>> {
+    let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
+    path_for_file(file.as_ref(), height, include_filename, cx)
+}
+
+fn path_for_file<'a>(
+    file: &'a dyn language::File,
+    mut height: usize,
+    include_filename: bool,
+    cx: &'a AppContext,
+) -> Option<Cow<'a, Path>> {
+    // Ensure we always render at least the filename.
+    height += 1;
+
+    let mut prefix = file.path().as_ref();
+    while height > 0 {
+        if let Some(parent) = prefix.parent() {
+            prefix = parent;
+            height -= 1;
+        } else {
+            break;
+        }
+    }
+
+    // Here we could have just always used `full_path`, but that is very
+    // allocation-heavy and so we try to use a `Cow<Path>` if we haven't
+    // traversed all the way up to the worktree's root.
+    if height > 0 {
+        let full_path = file.full_path(cx);
+        if include_filename {
+            Some(full_path.into())
+        } else {
+            Some(full_path.parent()?.to_path_buf().into())
+        }
+    } else {
+        let mut path = file.path().strip_prefix(prefix).ok()?;
+        if !include_filename {
+            path = path.parent()?;
+        }
+        Some(path.into())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::AppContext;
+    use std::{
+        path::{Path, PathBuf},
+        sync::Arc,
+        time::SystemTime,
+    };
+
+    #[gpui::test]
+    fn test_path_for_file(cx: &mut AppContext) {
+        let file = TestFile {
+            path: Path::new("").into(),
+            full_path: PathBuf::from(""),
+        };
+        assert_eq!(path_for_file(&file, 0, false, cx), None);
+    }
+
+    struct TestFile {
+        path: Arc<Path>,
+        full_path: PathBuf,
+    }
+
+    impl language::File for TestFile {
+        fn path(&self) -> &Arc<Path> {
+            &self.path
+        }
+
+        fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
+            self.full_path.clone()
+        }
+
+        fn as_local(&self) -> Option<&dyn language::LocalFile> {
+            unimplemented!()
+        }
+
+        fn mtime(&self) -> SystemTime {
+            unimplemented!()
+        }
+
+        fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
+            unimplemented!()
+        }
+
+        fn worktree_id(&self) -> usize {
+            0
+        }
+
+        fn is_deleted(&self) -> bool {
+            unimplemented!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            unimplemented!()
+        }
+
+        fn to_proto(&self) -> rpc::proto::File {
+            unimplemented!()
+        }
+    }
+}
@@ -0,0 +1,1269 @@
+use crate::{
+    display_map::DisplaySnapshot,
+    element::PointForPosition,
+    hover_popover::{self, InlayHover},
+    Anchor, DisplayPoint, Editor, EditorSnapshot, InlayId, SelectPhase,
+};
+use gpui::{Task, ViewContext};
+use language::{Bias, ToOffset};
+use lsp::LanguageServerId;
+use project::{
+    HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink,
+    ResolveState,
+};
+use std::ops::Range;
+use util::TryFutureExt;
+
+#[derive(Debug, Default)]
+pub struct LinkGoToDefinitionState {
+    pub last_trigger_point: Option<TriggerPoint>,
+    pub symbol_range: Option<RangeInEditor>,
+    pub kind: Option<LinkDefinitionKind>,
+    pub definitions: Vec<GoToDefinitionLink>,
+    pub task: Option<Task<Option<()>>>,
+}
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub enum RangeInEditor {
+    Text(Range<Anchor>),
+    Inlay(InlayHighlight),
+}
+
+impl RangeInEditor {
+    pub fn as_text_range(&self) -> Option<Range<Anchor>> {
+        match self {
+            Self::Text(range) => Some(range.clone()),
+            Self::Inlay(_) => None,
+        }
+    }
+
+    fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
+        match (self, trigger_point) {
+            (Self::Text(range), TriggerPoint::Text(point)) => {
+                let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
+                point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge()
+            }
+            (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => {
+                highlight.inlay == point.inlay
+                    && highlight.range.contains(&point.range.start)
+                    && highlight.range.contains(&point.range.end)
+            }
+            (Self::Inlay(_), TriggerPoint::Text(_))
+            | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum GoToDefinitionTrigger {
+    Text(DisplayPoint),
+    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone)]
+pub enum GoToDefinitionLink {
+    Text(LocationLink),
+    InlayHint(lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct InlayHighlight {
+    pub inlay: InlayId,
+    pub inlay_position: Anchor,
+    pub range: Range<usize>,
+}
+
+#[derive(Debug, Clone)]
+pub enum TriggerPoint {
+    Text(Anchor),
+    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
+}
+
+impl TriggerPoint {
+    pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
+        match self {
+            TriggerPoint::Text(_) => {
+                if shift {
+                    LinkDefinitionKind::Type
+                } else {
+                    LinkDefinitionKind::Symbol
+                }
+            }
+            TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type,
+        }
+    }
+
+    fn anchor(&self) -> &Anchor {
+        match self {
+            TriggerPoint::Text(anchor) => anchor,
+            TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position,
+        }
+    }
+}
+
+pub fn update_go_to_definition_link(
+    editor: &mut Editor,
+    origin: Option<GoToDefinitionTrigger>,
+    cmd_held: bool,
+    shift_held: bool,
+    cx: &mut ViewContext<Editor>,
+) {
+    let pending_nonempty_selection = editor.has_pending_nonempty_selection();
+
+    // Store new mouse point as an anchor
+    let snapshot = editor.snapshot(cx);
+    let trigger_point = match origin {
+        Some(GoToDefinitionTrigger::Text(p)) => {
+            Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before(
+                p.to_offset(&snapshot.display_snapshot, Bias::Left),
+            )))
+        }
+        Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => {
+            Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id))
+        }
+        None => None,
+    };
+
+    // If the new point is the same as the previously stored one, return early
+    if let (Some(a), Some(b)) = (
+        &trigger_point,
+        &editor.link_go_to_definition_state.last_trigger_point,
+    ) {
+        match (a, b) {
+            (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => {
+                if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() {
+                    return;
+                }
+            }
+            (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => {
+                if range_a == range_b {
+                    return;
+                }
+            }
+            _ => {}
+        }
+    }
+
+    editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone();
+
+    if pending_nonempty_selection {
+        hide_link_definition(editor, cx);
+        return;
+    }
+
+    if cmd_held {
+        if let Some(trigger_point) = trigger_point {
+            let kind = trigger_point.definition_kind(shift_held);
+            show_link_definition(kind, editor, trigger_point, snapshot, cx);
+            return;
+        }
+    }
+
+    hide_link_definition(editor, cx);
+}
+
+pub fn update_inlay_link_and_hover_points(
+    snapshot: &DisplaySnapshot,
+    point_for_position: PointForPosition,
+    editor: &mut Editor,
+    cmd_held: bool,
+    shift_held: bool,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) {
+    let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
+        Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
+    } else {
+        None
+    };
+    let mut go_to_definition_updated = false;
+    let mut hover_updated = false;
+    if let Some(hovered_offset) = hovered_offset {
+        let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
+        let previous_valid_anchor = buffer_snapshot.anchor_at(
+            point_for_position.previous_valid.to_point(snapshot),
+            Bias::Left,
+        );
+        let next_valid_anchor = buffer_snapshot.anchor_at(
+            point_for_position.next_valid.to_point(snapshot),
+            Bias::Right,
+        );
+        if let Some(hovered_hint) = editor
+            .visible_inlay_hints(cx)
+            .into_iter()
+            .skip_while(|hint| {
+                hint.position
+                    .cmp(&previous_valid_anchor, &buffer_snapshot)
+                    .is_lt()
+            })
+            .take_while(|hint| {
+                hint.position
+                    .cmp(&next_valid_anchor, &buffer_snapshot)
+                    .is_le()
+            })
+            .max_by_key(|hint| hint.id)
+        {
+            let inlay_hint_cache = editor.inlay_hint_cache();
+            let excerpt_id = previous_valid_anchor.excerpt_id;
+            if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
+                match cached_hint.resolve_state {
+                    ResolveState::CanResolve(_, _) => {
+                        if let Some(buffer_id) = previous_valid_anchor.buffer_id {
+                            inlay_hint_cache.spawn_hint_resolve(
+                                buffer_id,
+                                excerpt_id,
+                                hovered_hint.id,
+                                cx,
+                            );
+                        }
+                    }
+                    ResolveState::Resolved => {
+                        let mut extra_shift_left = 0;
+                        let mut extra_shift_right = 0;
+                        if cached_hint.padding_left {
+                            extra_shift_left += 1;
+                            extra_shift_right += 1;
+                        }
+                        if cached_hint.padding_right {
+                            extra_shift_right += 1;
+                        }
+                        match cached_hint.label {
+                            project::InlayHintLabel::String(_) => {
+                                if let Some(tooltip) = cached_hint.tooltip {
+                                    hover_popover::hover_at_inlay(
+                                        editor,
+                                        InlayHover {
+                                            excerpt: excerpt_id,
+                                            tooltip: match tooltip {
+                                                InlayHintTooltip::String(text) => HoverBlock {
+                                                    text,
+                                                    kind: HoverBlockKind::PlainText,
+                                                },
+                                                InlayHintTooltip::MarkupContent(content) => {
+                                                    HoverBlock {
+                                                        text: content.value,
+                                                        kind: content.kind,
+                                                    }
+                                                }
+                                            },
+                                            range: InlayHighlight {
+                                                inlay: hovered_hint.id,
+                                                inlay_position: hovered_hint.position,
+                                                range: extra_shift_left
+                                                    ..hovered_hint.text.len() + extra_shift_right,
+                                            },
+                                        },
+                                        cx,
+                                    );
+                                    hover_updated = true;
+                                }
+                            }
+                            project::InlayHintLabel::LabelParts(label_parts) => {
+                                let hint_start =
+                                    snapshot.anchor_to_inlay_offset(hovered_hint.position);
+                                if let Some((hovered_hint_part, part_range)) =
+                                    hover_popover::find_hovered_hint_part(
+                                        label_parts,
+                                        hint_start,
+                                        hovered_offset,
+                                    )
+                                {
+                                    let highlight_start =
+                                        (part_range.start - hint_start).0 + extra_shift_left;
+                                    let highlight_end =
+                                        (part_range.end - hint_start).0 + extra_shift_right;
+                                    let highlight = InlayHighlight {
+                                        inlay: hovered_hint.id,
+                                        inlay_position: hovered_hint.position,
+                                        range: highlight_start..highlight_end,
+                                    };
+                                    if let Some(tooltip) = hovered_hint_part.tooltip {
+                                        hover_popover::hover_at_inlay(
+                                            editor,
+                                            InlayHover {
+                                                excerpt: excerpt_id,
+                                                tooltip: match tooltip {
+                                                    InlayHintLabelPartTooltip::String(text) => {
+                                                        HoverBlock {
+                                                            text,
+                                                            kind: HoverBlockKind::PlainText,
+                                                        }
+                                                    }
+                                                    InlayHintLabelPartTooltip::MarkupContent(
+                                                        content,
+                                                    ) => HoverBlock {
+                                                        text: content.value,
+                                                        kind: content.kind,
+                                                    },
+                                                },
+                                                range: highlight.clone(),
+                                            },
+                                            cx,
+                                        );
+                                        hover_updated = true;
+                                    }
+                                    if let Some((language_server_id, location)) =
+                                        hovered_hint_part.location
+                                    {
+                                        go_to_definition_updated = true;
+                                        update_go_to_definition_link(
+                                            editor,
+                                            Some(GoToDefinitionTrigger::InlayHint(
+                                                highlight,
+                                                location,
+                                                language_server_id,
+                                            )),
+                                            cmd_held,
+                                            shift_held,
+                                            cx,
+                                        );
+                                    }
+                                }
+                            }
+                        };
+                    }
+                    ResolveState::Resolving => {}
+                }
+            }
+        }
+    }
+
+    if !go_to_definition_updated {
+        update_go_to_definition_link(editor, None, cmd_held, shift_held, cx);
+    }
+    if !hover_updated {
+        hover_popover::hover_at(editor, None, cx);
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum LinkDefinitionKind {
+    Symbol,
+    Type,
+}
+
+pub fn show_link_definition(
+    definition_kind: LinkDefinitionKind,
+    editor: &mut Editor,
+    trigger_point: TriggerPoint,
+    snapshot: EditorSnapshot,
+    cx: &mut ViewContext<Editor>,
+) {
+    let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind);
+    if !same_kind {
+        hide_link_definition(editor, cx);
+    }
+
+    if editor.pending_rename.is_some() {
+        return;
+    }
+
+    let trigger_anchor = trigger_point.anchor();
+    let (buffer, buffer_position) = if let Some(output) = editor
+        .buffer
+        .read(cx)
+        .text_anchor_for_position(trigger_anchor.clone(), cx)
+    {
+        output
+    } else {
+        return;
+    };
+
+    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
+        .buffer()
+        .read(cx)
+        .excerpt_containing(trigger_anchor.clone(), cx)
+    {
+        excerpt_id
+    } else {
+        return;
+    };
+
+    let project = if let Some(project) = editor.project.clone() {
+        project
+    } else {
+        return;
+    };
+
+    // Don't request again if the location is within the symbol region of a previous request with the same kind
+    if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
+        if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) {
+            return;
+        }
+    }
+
+    let task = cx.spawn(|this, mut cx| {
+        async move {
+            let result = match &trigger_point {
+                TriggerPoint::Text(_) => {
+                    // query the LSP for definition info
+                    cx.update(|cx| {
+                        project.update(cx, |project, cx| match definition_kind {
+                            LinkDefinitionKind::Symbol => {
+                                project.definition(&buffer, buffer_position, cx)
+                            }
+
+                            LinkDefinitionKind::Type => {
+                                project.type_definition(&buffer, buffer_position, cx)
+                            }
+                        })
+                    })
+                    .await
+                    .ok()
+                    .map(|definition_result| {
+                        (
+                            definition_result.iter().find_map(|link| {
+                                link.origin.as_ref().map(|origin| {
+                                    let start = snapshot
+                                        .buffer_snapshot
+                                        .anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
+                                    let end = snapshot
+                                        .buffer_snapshot
+                                        .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
+                                    RangeInEditor::Text(start..end)
+                                })
+                            }),
+                            definition_result
+                                .into_iter()
+                                .map(GoToDefinitionLink::Text)
+                                .collect(),
+                        )
+                    })
+                }
+                TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
+                    Some(RangeInEditor::Inlay(highlight.clone())),
+                    vec![GoToDefinitionLink::InlayHint(
+                        lsp_location.clone(),
+                        *server_id,
+                    )],
+                )),
+            };
+
+            this.update(&mut cx, |this, cx| {
+                // Clear any existing highlights
+                this.clear_highlights::<LinkGoToDefinitionState>(cx);
+                this.link_go_to_definition_state.kind = Some(definition_kind);
+                this.link_go_to_definition_state.symbol_range = result
+                    .as_ref()
+                    .and_then(|(symbol_range, _)| symbol_range.clone());
+
+                if let Some((symbol_range, definitions)) = result {
+                    this.link_go_to_definition_state.definitions = definitions.clone();
+
+                    let buffer_snapshot = buffer.read(cx).snapshot();
+
+                    // Only show highlight if there exists a definition to jump to that doesn't contain
+                    // the current location.
+                    let any_definition_does_not_contain_current_location =
+                        definitions.iter().any(|definition| {
+                            match &definition {
+                                GoToDefinitionLink::Text(link) => {
+                                    if link.target.buffer == buffer {
+                                        let range = &link.target.range;
+                                        // Expand range by one character as lsp definition ranges include positions adjacent
+                                        // but not contained by the symbol range
+                                        let start = buffer_snapshot.clip_offset(
+                                            range
+                                                .start
+                                                .to_offset(&buffer_snapshot)
+                                                .saturating_sub(1),
+                                            Bias::Left,
+                                        );
+                                        let end = buffer_snapshot.clip_offset(
+                                            range.end.to_offset(&buffer_snapshot) + 1,
+                                            Bias::Right,
+                                        );
+                                        let offset = buffer_position.to_offset(&buffer_snapshot);
+                                        !(start <= offset && end >= offset)
+                                    } else {
+                                        true
+                                    }
+                                }
+                                GoToDefinitionLink::InlayHint(_, _) => true,
+                            }
+                        });
+
+                    if any_definition_does_not_contain_current_location {
+                        // Highlight symbol using theme link definition highlight style
+                        let style = theme::current(cx).editor.link_definition;
+                        let highlight_range =
+                            symbol_range.unwrap_or_else(|| match &trigger_point {
+                                TriggerPoint::Text(trigger_anchor) => {
+                                    let snapshot = &snapshot.buffer_snapshot;
+                                    // If no symbol range returned from language server, use the surrounding word.
+                                    let (offset_range, _) =
+                                        snapshot.surrounding_word(*trigger_anchor);
+                                    RangeInEditor::Text(
+                                        snapshot.anchor_before(offset_range.start)
+                                            ..snapshot.anchor_after(offset_range.end),
+                                    )
+                                }
+                                TriggerPoint::InlayHint(highlight, _, _) => {
+                                    RangeInEditor::Inlay(highlight.clone())
+                                }
+                            });
+
+                        match highlight_range {
+                            RangeInEditor::Text(text_range) => this
+                                .highlight_text::<LinkGoToDefinitionState>(
+                                    vec![text_range],
+                                    style,
+                                    cx,
+                                ),
+                            RangeInEditor::Inlay(highlight) => this
+                                .highlight_inlays::<LinkGoToDefinitionState>(
+                                    vec![highlight],
+                                    style,
+                                    cx,
+                                ),
+                        }
+                    } else {
+                        hide_link_definition(this, cx);
+                    }
+                }
+            })?;
+
+            Ok::<_, anyhow::Error>(())
+        }
+        .log_err()
+    });
+
+    editor.link_go_to_definition_state.task = Some(task);
+}
+
+pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
+    if editor.link_go_to_definition_state.symbol_range.is_some()
+        || !editor.link_go_to_definition_state.definitions.is_empty()
+    {
+        editor.link_go_to_definition_state.symbol_range.take();
+        editor.link_go_to_definition_state.definitions.clear();
+        cx.notify();
+    }
+
+    editor.link_go_to_definition_state.task = None;
+
+    editor.clear_highlights::<LinkGoToDefinitionState>(cx);
+}
+
+pub fn go_to_fetched_definition(
+    editor: &mut Editor,
+    point: PointForPosition,
+    split: bool,
+    cx: &mut ViewContext<Editor>,
+) {
+    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx);
+}
+
+pub fn go_to_fetched_type_definition(
+    editor: &mut Editor,
+    point: PointForPosition,
+    split: bool,
+    cx: &mut ViewContext<Editor>,
+) {
+    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx);
+}
+
+fn go_to_fetched_definition_of_kind(
+    kind: LinkDefinitionKind,
+    editor: &mut Editor,
+    point: PointForPosition,
+    split: bool,
+    cx: &mut ViewContext<Editor>,
+) {
+    let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
+    hide_link_definition(editor, cx);
+    let cached_definitions_kind = editor.link_go_to_definition_state.kind;
+
+    let is_correct_kind = cached_definitions_kind == Some(kind);
+    if !cached_definitions.is_empty() && is_correct_kind {
+        if !editor.focused {
+            cx.focus_self();
+        }
+
+        editor.navigate_to_definitions(cached_definitions, split, cx);
+    } else {
+        editor.select(
+            SelectPhase::Begin {
+                position: point.next_valid,
+                add: false,
+                click_count: 1,
+            },
+            cx,
+        );
+
+        if point.as_valid().is_some() {
+            match kind {
+                LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
+                LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::ToDisplayPoint,
+        editor_tests::init_test,
+        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+        test::editor_lsp_test_context::EditorLspTestContext,
+    };
+    use futures::StreamExt;
+    use gpui::{
+        platform::{self, Modifiers, ModifiersChangedEvent},
+        View,
+    };
+    use indoc::indoc;
+    use language::language_settings::InlayHintSettings;
+    use lsp::request::{GotoDefinition, GotoTypeDefinition};
+    use util::assert_set_eq;
+
+    #[gpui::test]
+    async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            struct A;
+            let vห‡ariable = A;
+        "});
+
+        // Basic hold cmd+shift, expect highlight in region if response contains type definition
+        let hover_point = cx.display_point(indoc! {"
+            struct A;
+            let vห‡ariable = A;
+        "});
+        let symbol_range = cx.lsp_range(indoc! {"
+            struct A;
+            let ยซvariableยป = A;
+        "});
+        let target_range = cx.lsp_range(indoc! {"
+            struct ยซAยป;
+            let variable = A;
+        "});
+
+        let mut requests =
+            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+                    lsp::LocationLink {
+                        origin_selection_range: Some(symbol_range),
+                        target_uri: url.clone(),
+                        target_range,
+                        target_selection_range: target_range,
+                    },
+                ])))
+            });
+
+        // Press cmd+shift to trigger highlight
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                true,
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.foreground().run_until_parked();
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            struct A;
+            let ยซvariableยป = A;
+        "});
+
+        // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
+        cx.update_editor(|editor, cx| {
+            editor.modifiers_changed(
+                &platform::ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        cmd: true,
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                cx,
+            );
+        });
+        // Assert no link highlights
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            struct A;
+            let variable = A;
+        "});
+
+        // Cmd+shift click without existing definition requests and jumps
+        let hover_point = cx.display_point(indoc! {"
+            struct A;
+            let vห‡ariable = A;
+        "});
+        let target_range = cx.lsp_range(indoc! {"
+            struct ยซAยป;
+            let variable = A;
+        "});
+
+        let mut requests =
+            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+                    lsp::LocationLink {
+                        origin_selection_range: None,
+                        target_uri: url,
+                        target_range,
+                        target_selection_range: target_range,
+                    },
+                ])))
+            });
+
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
+        });
+        requests.next().await;
+        cx.foreground().run_until_parked();
+
+        cx.assert_editor_state(indoc! {"
+            struct ยซAห‡ยป;
+            let variable = A;
+        "});
+    }
+
+    #[gpui::test]
+    async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            fn ห‡test() { do_work(); }
+            fn do_work() { test(); }
+        "});
+
+        // Basic hold cmd, expect highlight in region if response contains definition
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { do_wห‡ork(); }
+            fn do_work() { test(); }
+        "});
+        let symbol_range = cx.lsp_range(indoc! {"
+            fn test() { ยซdo_workยป(); }
+            fn do_work() { test(); }
+        "});
+        let target_range = cx.lsp_range(indoc! {"
+            fn test() { do_work(); }
+            fn ยซdo_workยป() { test(); }
+        "});
+
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: Some(symbol_range),
+                    target_uri: url.clone(),
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
+
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.foreground().run_until_parked();
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { ยซdo_workยป(); }
+            fn do_work() { test(); }
+        "});
+
+        // Unpress cmd causes highlight to go away
+        cx.update_editor(|editor, cx| {
+            editor.modifiers_changed(&Default::default(), cx);
+        });
+
+        // Assert no link highlights
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { test(); }
+        "});
+
+        // Response without source range still highlights word
+        cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    // No origin range
+                    origin_selection_range: None,
+                    target_uri: url.clone(),
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.foreground().run_until_parked();
+
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { ยซdo_workยป(); }
+            fn do_work() { test(); }
+        "});
+
+        // Moving mouse to location with no response dismisses highlight
+        let hover_point = cx.display_point(indoc! {"
+            fห‡n test() { do_work(); }
+            fn do_work() { test(); }
+        "});
+        let mut requests = cx
+            .lsp
+            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
+                // No definitions returned
+                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
+            });
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.foreground().run_until_parked();
+
+        // Assert no link highlights
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { test(); }
+        "});
+
+        // Move mouse without cmd and then pressing cmd triggers highlight
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { teห‡st(); }
+        "});
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                false,
+                false,
+                cx,
+            );
+        });
+        cx.foreground().run_until_parked();
+
+        // Assert no link highlights
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { test(); }
+        "});
+
+        let symbol_range = cx.lsp_range(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { ยซtestยป(); }
+        "});
+        let target_range = cx.lsp_range(indoc! {"
+            fn ยซtestยป() { do_work(); }
+            fn do_work() { test(); }
+        "});
+
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: Some(symbol_range),
+                    target_uri: url,
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
+        cx.update_editor(|editor, cx| {
+            editor.modifiers_changed(
+                &ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        cmd: true,
+                        ..Default::default()
+                    },
+                },
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.foreground().run_until_parked();
+
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { ยซtestยป(); }
+        "});
+
+        // Deactivating the window dismisses the highlight
+        cx.update_workspace(|workspace, cx| {
+            workspace.on_window_activation_changed(false, cx);
+        });
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { test(); }
+        "});
+
+        // Moving the mouse restores the highlights.
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground().run_until_parked();
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { ยซtestยป(); }
+        "});
+
+        // Moving again within the same symbol range doesn't re-request
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { tesห‡t(); }
+        "});
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground().run_until_parked();
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { ยซtestยป(); }
+        "});
+
+        // Cmd click with existing definition doesn't re-request and dismisses highlight
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
+        });
+        // Assert selection moved to to definition
+        cx.lsp
+            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
+                // Empty definition response to make sure we aren't hitting the lsp and using
+                // the cached location instead
+                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
+            });
+        cx.foreground().run_until_parked();
+        cx.assert_editor_state(indoc! {"
+            fn ยซtestห‡ยป() { do_work(); }
+            fn do_work() { test(); }
+        "});
+
+        // Assert no link highlights after jump
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { test(); }
+        "});
+
+        // Cmd click without existing definition requests and jumps
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { do_wห‡ork(); }
+            fn do_work() { test(); }
+        "});
+        let target_range = cx.lsp_range(indoc! {"
+            fn test() { do_work(); }
+            fn ยซdo_workยป() { test(); }
+        "});
+
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: None,
+                    target_uri: url,
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
+        });
+        requests.next().await;
+        cx.foreground().run_until_parked();
+        cx.assert_editor_state(indoc! {"
+            fn test() { do_work(); }
+            fn ยซdo_workห‡ยป() { test(); }
+        "});
+
+        // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
+        // 2. Selection is completed, hovering
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { do_wห‡ork(); }
+            fn do_work() { test(); }
+        "});
+        let target_range = cx.lsp_range(indoc! {"
+            fn test() { do_work(); }
+            fn ยซdo_workยป() { test(); }
+        "});
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: None,
+                    target_uri: url,
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
+
+        // create a pending selection
+        let selection_range = cx.ranges(indoc! {"
+            fn ยซtest() { do_wยปork(); }
+            fn do_work() { test(); }
+        "})[0]
+            .clone();
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.buffer().read(cx).snapshot(cx);
+            let anchor_range = snapshot.anchor_before(selection_range.start)
+                ..snapshot.anchor_after(selection_range.end);
+            editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
+                s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
+            });
+        });
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground().run_until_parked();
+        assert!(requests.try_next().is_err());
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            fn test() { do_work(); }
+            fn do_work() { test(); }
+        "});
+        cx.foreground().run_until_parked();
+    }
+
+    #[gpui::test]
+    async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+        cx.set_state(indoc! {"
+            struct TestStruct;
+
+            fn main() {
+                let variableห‡ = TestStruct;
+            }
+        "});
+        let hint_start_offset = cx.ranges(indoc! {"
+            struct TestStruct;
+
+            fn main() {
+                let variableห‡ = TestStruct;
+            }
+        "})[0]
+            .start;
+        let hint_position = cx.to_lsp(hint_start_offset);
+        let target_range = cx.lsp_range(indoc! {"
+            struct ยซTestStructยป;
+
+            fn main() {
+                let variable = TestStruct;
+            }
+        "});
+
+        let expected_uri = cx.buffer_lsp_url.clone();
+        let hint_label = ": TestStruct";
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let expected_uri = expected_uri.clone();
+                async move {
+                    assert_eq!(params.text_document.uri, expected_uri);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: hint_position,
+                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+                            value: hint_label.to_string(),
+                            location: Some(lsp::Location {
+                                uri: params.text_document.uri,
+                                range: target_range,
+                            }),
+                            ..Default::default()
+                        }]),
+                        kind: Some(lsp::InlayHintKind::TYPE),
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: Some(false),
+                        padding_right: Some(false),
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let expected_layers = vec![hint_label.to_string()];
+            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+        });
+
+        let inlay_range = cx
+            .ranges(indoc! {"
+            struct TestStruct;
+
+            fn main() {
+                let variableยซ ยป= TestStruct;
+            }
+        "})
+            .get(0)
+            .cloned()
+            .unwrap();
+        let hint_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column() + (hint_label.len() / 2) as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        // Press cmd to trigger highlight
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                hint_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let actual_highlights = snapshot
+                .inlay_highlights::<LinkGoToDefinitionState>()
+                .into_iter()
+                .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
+                .collect::<Vec<_>>();
+
+            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+            let expected_highlight = InlayHighlight {
+                inlay: InlayId::Hint(0),
+                inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                range: 0..hint_label.len(),
+            };
+            assert_set_eq!(actual_highlights, vec![&expected_highlight]);
+        });
+
+        // Unpress cmd causes highlight to go away
+        cx.update_editor(|editor, cx| {
+            editor.modifiers_changed(
+                &platform::ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        cmd: false,
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                cx,
+            );
+        });
+        // Assert no link highlights
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let actual_ranges = snapshot
+                .text_highlight_ranges::<LinkGoToDefinitionState>()
+                .map(|ranges| ranges.as_ref().clone().1)
+                .unwrap_or_default();
+
+            assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
+        });
+
+        // Cmd+click without existing definition requests and jumps
+        cx.update_editor(|editor, cx| {
+            editor.modifiers_changed(
+                &platform::ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        cmd: true,
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                cx,
+            );
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                hint_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
+        });
+        cx.foreground().run_until_parked();
+        cx.assert_editor_state(indoc! {"
+            struct ยซTestStructห‡ยป;
+
+            fn main() {
+                let variable = TestStruct;
+            }
+        "});
+    }
+}

crates/editor2/src/mouse_context_menu.rs ๐Ÿ”—

@@ -0,0 +1,96 @@
+use crate::{
+    DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
+    Rename, RevealInFinder, SelectMode, ToggleCodeActions,
+};
+use context_menu::ContextMenuItem;
+use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext};
+
+pub fn deploy_context_menu(
+    editor: &mut Editor,
+    position: Vector2F,
+    point: DisplayPoint,
+    cx: &mut ViewContext<Editor>,
+) {
+    if !editor.focused {
+        cx.focus_self();
+    }
+
+    // Don't show context menu for inline editors
+    if editor.mode() != EditorMode::Full {
+        return;
+    }
+
+    // Don't show the context menu if there isn't a project associated with this editor
+    if editor.project.is_none() {
+        return;
+    }
+
+    // Move the cursor to the clicked location so that dispatched actions make sense
+    editor.change_selections(None, cx, |s| {
+        s.clear_disjoint();
+        s.set_pending_display_range(point..point, SelectMode::Character);
+    });
+
+    editor.mouse_context_menu.update(cx, |menu, cx| {
+        menu.show(
+            position,
+            AnchorCorner::TopLeft,
+            vec![
+                ContextMenuItem::action("Rename Symbol", Rename),
+                ContextMenuItem::action("Go to Definition", GoToDefinition),
+                ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
+                ContextMenuItem::action("Find All References", FindAllReferences),
+                ContextMenuItem::action(
+                    "Code Actions",
+                    ToggleCodeActions {
+                        deployed_from_indicator: false,
+                    },
+                ),
+                ContextMenuItem::Separator,
+                ContextMenuItem::action("Reveal in Finder", RevealInFinder),
+            ],
+            cx,
+        );
+    });
+    cx.notify();
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            fn teห‡st() {
+                do_work();
+            }
+        "});
+        let point = cx.display_point(indoc! {"
+            fn test() {
+                do_wห‡ork();
+            }
+        "});
+        cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
+
+        cx.assert_editor_state(indoc! {"
+            fn test() {
+                do_wห‡ork();
+            }
+        "});
+        cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
+    }
+}

crates/editor2/src/movement.rs ๐Ÿ”—

@@ -0,0 +1,927 @@
+use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
+use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
+use gpui::{FontCache, TextLayoutCache};
+use language::Point;
+use std::{ops::Range, sync::Arc};
+
+#[derive(Debug, PartialEq)]
+pub enum FindRange {
+    SingleLine,
+    MultiLine,
+}
+
+/// TextLayoutDetails encompasses everything we need to move vertically
+/// taking into account variable width characters.
+pub struct TextLayoutDetails {
+    pub font_cache: Arc<FontCache>,
+    pub text_layout_cache: Arc<TextLayoutCache>,
+    pub editor_style: EditorStyle,
+}
+
+pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+    if point.column() > 0 {
+        *point.column_mut() -= 1;
+    } else if point.row() > 0 {
+        *point.row_mut() -= 1;
+        *point.column_mut() = map.line_len(point.row());
+    }
+    map.clip_point(point, Bias::Left)
+}
+
+pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+    if point.column() > 0 {
+        *point.column_mut() -= 1;
+    }
+    map.clip_point(point, Bias::Left)
+}
+
+pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+    let max_column = map.line_len(point.row());
+    if point.column() < max_column {
+        *point.column_mut() += 1;
+    } else if point.row() < map.max_point().row() {
+        *point.row_mut() += 1;
+        *point.column_mut() = 0;
+    }
+    map.clip_point(point, Bias::Right)
+}
+
+pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+    *point.column_mut() += 1;
+    map.clip_point(point, Bias::Right)
+}
+
+pub fn up(
+    map: &DisplaySnapshot,
+    start: DisplayPoint,
+    goal: SelectionGoal,
+    preserve_column_at_start: bool,
+    text_layout_details: &TextLayoutDetails,
+) -> (DisplayPoint, SelectionGoal) {
+    up_by_rows(
+        map,
+        start,
+        1,
+        goal,
+        preserve_column_at_start,
+        text_layout_details,
+    )
+}
+
+pub fn down(
+    map: &DisplaySnapshot,
+    start: DisplayPoint,
+    goal: SelectionGoal,
+    preserve_column_at_end: bool,
+    text_layout_details: &TextLayoutDetails,
+) -> (DisplayPoint, SelectionGoal) {
+    down_by_rows(
+        map,
+        start,
+        1,
+        goal,
+        preserve_column_at_end,
+        text_layout_details,
+    )
+}
+
+pub fn up_by_rows(
+    map: &DisplaySnapshot,
+    start: DisplayPoint,
+    row_count: u32,
+    goal: SelectionGoal,
+    preserve_column_at_start: bool,
+    text_layout_details: &TextLayoutDetails,
+) -> (DisplayPoint, SelectionGoal) {
+    let mut goal_x = match goal {
+        SelectionGoal::HorizontalPosition(x) => x,
+        SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
+        SelectionGoal::HorizontalRange { end, .. } => end,
+        _ => map.x_for_point(start, text_layout_details),
+    };
+
+    let prev_row = start.row().saturating_sub(row_count);
+    let mut point = map.clip_point(
+        DisplayPoint::new(prev_row, map.line_len(prev_row)),
+        Bias::Left,
+    );
+    if point.row() < start.row() {
+        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+    } else if preserve_column_at_start {
+        return (start, goal);
+    } else {
+        point = DisplayPoint::new(0, 0);
+        goal_x = 0.0;
+    }
+
+    let mut clipped_point = map.clip_point(point, Bias::Left);
+    if clipped_point.row() < point.row() {
+        clipped_point = map.clip_point(point, Bias::Right);
+    }
+    (clipped_point, SelectionGoal::HorizontalPosition(goal_x))
+}
+
+pub fn down_by_rows(
+    map: &DisplaySnapshot,
+    start: DisplayPoint,
+    row_count: u32,
+    goal: SelectionGoal,
+    preserve_column_at_end: bool,
+    text_layout_details: &TextLayoutDetails,
+) -> (DisplayPoint, SelectionGoal) {
+    let mut goal_x = match goal {
+        SelectionGoal::HorizontalPosition(x) => x,
+        SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
+        SelectionGoal::HorizontalRange { end, .. } => end,
+        _ => map.x_for_point(start, text_layout_details),
+    };
+
+    let new_row = start.row() + row_count;
+    let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
+    if point.row() > start.row() {
+        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+    } else if preserve_column_at_end {
+        return (start, goal);
+    } else {
+        point = map.max_point();
+        goal_x = map.x_for_point(point, text_layout_details)
+    }
+
+    let mut clipped_point = map.clip_point(point, Bias::Right);
+    if clipped_point.row() > point.row() {
+        clipped_point = map.clip_point(point, Bias::Left);
+    }
+    (clipped_point, SelectionGoal::HorizontalPosition(goal_x))
+}
+
+pub fn line_beginning(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    stop_at_soft_boundaries: bool,
+) -> DisplayPoint {
+    let point = display_point.to_point(map);
+    let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
+    let line_start = map.prev_line_boundary(point).1;
+
+    if stop_at_soft_boundaries && display_point != soft_line_start {
+        soft_line_start
+    } else {
+        line_start
+    }
+}
+
+pub fn indented_line_beginning(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    stop_at_soft_boundaries: bool,
+) -> DisplayPoint {
+    let point = display_point.to_point(map);
+    let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
+    let indent_start = Point::new(
+        point.row,
+        map.buffer_snapshot.indent_size_for_line(point.row).len,
+    )
+    .to_display_point(map);
+    let line_start = map.prev_line_boundary(point).1;
+
+    if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
+    {
+        soft_line_start
+    } else if stop_at_soft_boundaries && display_point != indent_start {
+        indent_start
+    } else {
+        line_start
+    }
+}
+
+pub fn line_end(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    stop_at_soft_boundaries: bool,
+) -> DisplayPoint {
+    let soft_line_end = map.clip_point(
+        DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
+        Bias::Left,
+    );
+    if stop_at_soft_boundaries && display_point != soft_line_end {
+        soft_line_end
+    } else {
+        map.next_line_boundary(display_point.to_point(map)).1
+    }
+}
+
+pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    let raw_point = point.to_point(map);
+    let scope = map.buffer_snapshot.language_scope_at(raw_point);
+
+    find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
+        (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
+            || left == '\n'
+    })
+}
+
+pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    let raw_point = point.to_point(map);
+    let scope = map.buffer_snapshot.language_scope_at(raw_point);
+
+    find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
+        let is_word_start =
+            char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
+        let is_subword_start =
+            left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
+        is_word_start || is_subword_start || left == '\n'
+    })
+}
+
+pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    let raw_point = point.to_point(map);
+    let scope = map.buffer_snapshot.language_scope_at(raw_point);
+
+    find_boundary(map, point, FindRange::MultiLine, |left, right| {
+        (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
+            || right == '\n'
+    })
+}
+
+pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    let raw_point = point.to_point(map);
+    let scope = map.buffer_snapshot.language_scope_at(raw_point);
+
+    find_boundary(map, point, FindRange::MultiLine, |left, right| {
+        let is_word_end =
+            (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
+        let is_subword_end =
+            left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
+        is_word_end || is_subword_end || right == '\n'
+    })
+}
+
+pub fn start_of_paragraph(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    mut count: usize,
+) -> DisplayPoint {
+    let point = display_point.to_point(map);
+    if point.row == 0 {
+        return DisplayPoint::zero();
+    }
+
+    let mut found_non_blank_line = false;
+    for row in (0..point.row + 1).rev() {
+        let blank = map.buffer_snapshot.is_line_blank(row);
+        if found_non_blank_line && blank {
+            if count <= 1 {
+                return Point::new(row, 0).to_display_point(map);
+            }
+            count -= 1;
+            found_non_blank_line = false;
+        }
+
+        found_non_blank_line |= !blank;
+    }
+
+    DisplayPoint::zero()
+}
+
+pub fn end_of_paragraph(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    mut count: usize,
+) -> DisplayPoint {
+    let point = display_point.to_point(map);
+    if point.row == map.max_buffer_row() {
+        return map.max_point();
+    }
+
+    let mut found_non_blank_line = false;
+    for row in point.row..map.max_buffer_row() + 1 {
+        let blank = map.buffer_snapshot.is_line_blank(row);
+        if found_non_blank_line && blank {
+            if count <= 1 {
+                return Point::new(row, 0).to_display_point(map);
+            }
+            count -= 1;
+            found_non_blank_line = false;
+        }
+
+        found_non_blank_line |= !blank;
+    }
+
+    map.max_point()
+}
+
+/// Scans for a boundary preceding the given start point `from` until a boundary is found,
+/// indicated by the given predicate returning true.
+/// The predicate is called with the character to the left and right of the candidate boundary location.
+/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned.
+pub fn find_preceding_boundary(
+    map: &DisplaySnapshot,
+    from: DisplayPoint,
+    find_range: FindRange,
+    mut is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+    let mut prev_ch = None;
+    let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot);
+
+    for ch in map.buffer_snapshot.reversed_chars_at(offset) {
+        if find_range == FindRange::SingleLine && ch == '\n' {
+            break;
+        }
+        if let Some(prev_ch) = prev_ch {
+            if is_boundary(ch, prev_ch) {
+                break;
+            }
+        }
+
+        offset -= ch.len_utf8();
+        prev_ch = Some(ch);
+    }
+
+    map.clip_point(offset.to_display_point(map), Bias::Left)
+}
+
+/// Scans for a boundary following the given start point until a boundary is found, indicated by the
+/// given predicate returning true. The predicate is called with the character to the left and right
+/// of the candidate boundary location, and will be called with `\n` characters indicating the start
+/// or end of a line.
+pub fn find_boundary(
+    map: &DisplaySnapshot,
+    from: DisplayPoint,
+    find_range: FindRange,
+    mut is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+    let mut offset = from.to_offset(&map, Bias::Right);
+    let mut prev_ch = None;
+
+    for ch in map.buffer_snapshot.chars_at(offset) {
+        if find_range == FindRange::SingleLine && ch == '\n' {
+            break;
+        }
+        if let Some(prev_ch) = prev_ch {
+            if is_boundary(prev_ch, ch) {
+                break;
+            }
+        }
+
+        offset += ch.len_utf8();
+        prev_ch = Some(ch);
+    }
+    map.clip_point(offset.to_display_point(map), Bias::Right)
+}
+
+pub fn chars_after(
+    map: &DisplaySnapshot,
+    mut offset: usize,
+) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
+    map.buffer_snapshot.chars_at(offset).map(move |ch| {
+        let before = offset;
+        offset = offset + ch.len_utf8();
+        (ch, before..offset)
+    })
+}
+
+pub fn chars_before(
+    map: &DisplaySnapshot,
+    mut offset: usize,
+) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
+    map.buffer_snapshot
+        .reversed_chars_at(offset)
+        .map(move |ch| {
+            let after = offset;
+            offset = offset - ch.len_utf8();
+            (ch, offset..after)
+        })
+}
+
+pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
+    let raw_point = point.to_point(map);
+    let scope = map.buffer_snapshot.language_scope_at(raw_point);
+    let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
+    let text = &map.buffer_snapshot;
+    let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c));
+    let prev_char_kind = text
+        .reversed_chars_at(ix)
+        .next()
+        .map(|c| char_kind(&scope, c));
+    prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
+}
+
+pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
+    let position = map
+        .clip_point(position, Bias::Left)
+        .to_offset(map, Bias::Left);
+    let (range, _) = map.buffer_snapshot.surrounding_word(position);
+    let start = range
+        .start
+        .to_point(&map.buffer_snapshot)
+        .to_display_point(map);
+    let end = range
+        .end
+        .to_point(&map.buffer_snapshot)
+        .to_display_point(map);
+    start..end
+}
+
+pub fn split_display_range_by_lines(
+    map: &DisplaySnapshot,
+    range: Range<DisplayPoint>,
+) -> Vec<Range<DisplayPoint>> {
+    let mut result = Vec::new();
+
+    let mut start = range.start;
+    // Loop over all the covered rows until the one containing the range end
+    for row in range.start.row()..range.end.row() {
+        let row_end_column = map.line_len(row);
+        let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
+        if start != end {
+            result.push(start..end);
+        }
+        start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
+    }
+
+    // Add the final range from the start of the last end to the original range end.
+    result.push(start..range.end);
+
+    result
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::Inlay,
+        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+        Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
+    };
+    use project::Project;
+    use settings::SettingsStore;
+    use util::post_inc;
+
+    #[gpui::test]
+    fn test_previous_word_start(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                previous_word_start(&snapshot, display_points[1]),
+                display_points[0]
+            );
+        }
+
+        assert("\nห‡   ห‡lorem", cx);
+        assert("ห‡\nห‡   lorem", cx);
+        assert("    ห‡loremห‡", cx);
+        assert("ห‡    ห‡lorem", cx);
+        assert("    ห‡lorห‡em", cx);
+        assert("\nlorem\nห‡   ห‡ipsum", cx);
+        assert("\n\nห‡\nห‡", cx);
+        assert("    ห‡lorem  ห‡ipsum", cx);
+        assert("loremห‡-ห‡ipsum", cx);
+        assert("loremห‡-#$@ห‡ipsum", cx);
+        assert("ห‡lorem_ห‡ipsum", cx);
+        assert(" ห‡defฮณห‡", cx);
+        assert(" ห‡bcฮ”ห‡", cx);
+        assert(" abห‡โ€”โ€”ห‡cd", cx);
+    }
+
+    #[gpui::test]
+    fn test_previous_subword_start(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                previous_subword_start(&snapshot, display_points[1]),
+                display_points[0]
+            );
+        }
+
+        // Subword boundaries are respected
+        assert("lorem_ห‡ipห‡sum", cx);
+        assert("lorem_ห‡ipsumห‡", cx);
+        assert("ห‡lorem_ห‡ipsum", cx);
+        assert("lorem_ห‡ipsum_ห‡dolor", cx);
+        assert("loremห‡Ipห‡sum", cx);
+        assert("loremห‡Ipsumห‡", cx);
+
+        // Word boundaries are still respected
+        assert("\nห‡   ห‡lorem", cx);
+        assert("    ห‡loremห‡", cx);
+        assert("    ห‡lorห‡em", cx);
+        assert("\nlorem\nห‡   ห‡ipsum", cx);
+        assert("\n\nห‡\nห‡", cx);
+        assert("    ห‡lorem  ห‡ipsum", cx);
+        assert("loremห‡-ห‡ipsum", cx);
+        assert("loremห‡-#$@ห‡ipsum", cx);
+        assert(" ห‡defฮณห‡", cx);
+        assert(" bcห‡ฮ”ห‡", cx);
+        assert(" ห‡bcฮดห‡", cx);
+        assert(" abห‡โ€”โ€”ห‡cd", cx);
+    }
+
+    #[gpui::test]
+    fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(
+            marked_text: &str,
+            cx: &mut gpui::AppContext,
+            is_boundary: impl FnMut(char, char) -> bool,
+        ) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                find_preceding_boundary(
+                    &snapshot,
+                    display_points[1],
+                    FindRange::MultiLine,
+                    is_boundary
+                ),
+                display_points[0]
+            );
+        }
+
+        assert("abcห‡def\ngh\nijห‡k", cx, |left, right| {
+            left == 'c' && right == 'd'
+        });
+        assert("abcdef\nห‡gh\nijห‡k", cx, |left, right| {
+            left == '\n' && right == 'g'
+        });
+        let mut line_count = 0;
+        assert("abcdef\nห‡gh\nijห‡k", cx, |left, _| {
+            if left == '\n' {
+                line_count += 1;
+                line_count == 2
+            } else {
+                false
+            }
+        });
+    }
+
+    #[gpui::test]
+    fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        let input_text = "abcdefghijklmnopqrstuvwxys";
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let buffer = MultiBuffer::build_simple(input_text, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let display_map =
+            cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
+
+        // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
+        let mut id = 0;
+        let inlays = (0..buffer_snapshot.len())
+            .map(|offset| {
+                [
+                    Inlay {
+                        id: InlayId::Suggestion(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
+                        text: format!("test").into(),
+                    },
+                    Inlay {
+                        id: InlayId::Suggestion(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
+                        text: format!("test").into(),
+                    },
+                    Inlay {
+                        id: InlayId::Hint(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
+                        text: format!("test").into(),
+                    },
+                    Inlay {
+                        id: InlayId::Hint(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
+                        text: format!("test").into(),
+                    },
+                ]
+            })
+            .flatten()
+            .collect();
+        let snapshot = display_map.update(cx, |map, cx| {
+            map.splice_inlays(Vec::new(), inlays, cx);
+            map.snapshot(cx)
+        });
+
+        assert_eq!(
+            find_preceding_boundary(
+                &snapshot,
+                buffer_snapshot.len().to_display_point(&snapshot),
+                FindRange::MultiLine,
+                |left, _| left == 'e',
+            ),
+            snapshot
+                .buffer_snapshot
+                .offset_to_point(5)
+                .to_display_point(&snapshot),
+            "Should not stop at inlays when looking for boundaries"
+        );
+    }
+
+    #[gpui::test]
+    fn test_next_word_end(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                next_word_end(&snapshot, display_points[0]),
+                display_points[1]
+            );
+        }
+
+        assert("\nห‡   loremห‡", cx);
+        assert("    ห‡loremห‡", cx);
+        assert("    lorห‡emห‡", cx);
+        assert("    loremห‡    ห‡\nipsum\n", cx);
+        assert("\nห‡\nห‡\n\n", cx);
+        assert("loremห‡    ipsumห‡   ", cx);
+        assert("loremห‡-ห‡ipsum", cx);
+        assert("loremห‡#$@-ห‡ipsum", cx);
+        assert("loremห‡_ipsumห‡", cx);
+        assert(" ห‡bcฮ”ห‡", cx);
+        assert(" abห‡โ€”โ€”ห‡cd", cx);
+    }
+
+    #[gpui::test]
+    fn test_next_subword_end(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                next_subword_end(&snapshot, display_points[0]),
+                display_points[1]
+            );
+        }
+
+        // Subword boundaries are respected
+        assert("loห‡remห‡_ipsum", cx);
+        assert("ห‡loremห‡_ipsum", cx);
+        assert("loremห‡_ipsumห‡", cx);
+        assert("loremห‡_ipsumห‡_dolor", cx);
+        assert("loห‡remห‡Ipsum", cx);
+        assert("loremห‡Ipsumห‡Dolor", cx);
+
+        // Word boundaries are still respected
+        assert("\nห‡   loremห‡", cx);
+        assert("    ห‡loremห‡", cx);
+        assert("    lorห‡emห‡", cx);
+        assert("    loremห‡    ห‡\nipsum\n", cx);
+        assert("\nห‡\nห‡\n\n", cx);
+        assert("loremห‡    ipsumห‡   ", cx);
+        assert("loremห‡-ห‡ipsum", cx);
+        assert("loremห‡#$@-ห‡ipsum", cx);
+        assert("loremห‡_ipsumห‡", cx);
+        assert(" ห‡bcห‡ฮ”", cx);
+        assert(" abห‡โ€”โ€”ห‡cd", cx);
+    }
+
+    #[gpui::test]
+    fn test_find_boundary(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(
+            marked_text: &str,
+            cx: &mut gpui::AppContext,
+            is_boundary: impl FnMut(char, char) -> bool,
+        ) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                find_boundary(
+                    &snapshot,
+                    display_points[0],
+                    FindRange::MultiLine,
+                    is_boundary
+                ),
+                display_points[1]
+            );
+        }
+
+        assert("abcห‡def\ngh\nijห‡k", cx, |left, right| {
+            left == 'j' && right == 'k'
+        });
+        assert("abห‡cdef\ngh\nห‡ijk", cx, |left, right| {
+            left == '\n' && right == 'i'
+        });
+        let mut line_count = 0;
+        assert("abcห‡def\ngh\nห‡ijk", cx, |left, _| {
+            if left == '\n' {
+                line_count += 1;
+                line_count == 2
+            } else {
+                false
+            }
+        });
+    }
+
+    #[gpui::test]
+    fn test_surrounding_word(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                surrounding_word(&snapshot, display_points[1]),
+                display_points[0]..display_points[2],
+                "{}",
+                marked_text.to_string()
+            );
+        }
+
+        assert("ห‡ห‡loremห‡  ipsum", cx);
+        assert("ห‡loห‡remห‡  ipsum", cx);
+        assert("ห‡loremห‡ห‡  ipsum", cx);
+        assert("loremห‡ ห‡  ห‡ipsum", cx);
+        assert("lorem\nห‡ห‡ห‡\nipsum", cx);
+        assert("lorem\nห‡ห‡ipsumห‡", cx);
+        assert("loremห‡,ห‡ห‡ ipsum", cx);
+        assert("ห‡loremห‡ห‡, ipsum", cx);
+    }
+
+    #[gpui::test]
+    async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| {
+            init_test(cx);
+        });
+
+        let mut cx = EditorTestContext::new(cx).await;
+        let editor = cx.editor.clone();
+        let window = cx.window.clone();
+        cx.update_window(window, |cx| {
+            let text_layout_details =
+                editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
+
+            let family_id = cx
+                .font_cache()
+                .load_family(&["Helvetica"], &Default::default())
+                .unwrap();
+            let font_id = cx
+                .font_cache()
+                .select_font(family_id, &Default::default())
+                .unwrap();
+
+            let buffer =
+                cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
+            let multibuffer = cx.add_model(|cx| {
+                let mut multibuffer = MultiBuffer::new(0);
+                multibuffer.push_excerpts(
+                    buffer.clone(),
+                    [
+                        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,
+                );
+                multibuffer
+            });
+            let display_map =
+                cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
+            let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+
+            assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
+
+            let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
+
+            // Can't move up into the first excerpt's header
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(2, 2),
+                    SelectionGoal::HorizontalPosition(col_2_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::HorizontalPosition(0.0)
+                ),
+            );
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::None,
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::HorizontalPosition(0.0)
+                ),
+            );
+
+            let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
+
+            // Move up and down within first excerpt
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_4_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 3),
+                    SelectionGoal::HorizontalPosition(col_4_x)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(2, 3),
+                    SelectionGoal::HorizontalPosition(col_4_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_4_x)
+                ),
+            );
+
+            let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details);
+
+            // Move up and down across second excerpt's header
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(6, 5),
+                    SelectionGoal::HorizontalPosition(col_5_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_5_x)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_5_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(6, 5),
+                    SelectionGoal::HorizontalPosition(col_5_x)
+                ),
+            );
+
+            let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details);
+
+            // Can't move down off the end
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(7, 0),
+                    SelectionGoal::HorizontalPosition(0.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x)
+                ),
+            );
+        });
+    }
+
+    fn init_test(cx: &mut gpui::AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+        language::init(cx);
+        crate::init(cx);
+        Project::init_settings(cx);
+    }
+}

crates/editor2/src/persistence.rs ๐Ÿ”—

@@ -0,0 +1,83 @@
+use std::path::PathBuf;
+
+use db::sqlez_macros::sql;
+use db::{define_connection, query};
+
+use workspace::{ItemId, WorkspaceDb, WorkspaceId};
+
+define_connection!(
+    // Current schema shape using pseudo-rust syntax:
+    // editors(
+    //   item_id: usize,
+    //   workspace_id: usize,
+    //   path: PathBuf,
+    //   scroll_top_row: usize,
+    //   scroll_vertical_offset: f32,
+    //   scroll_horizontal_offset: f32,
+    // )
+    pub static ref DB: EditorDb<WorkspaceDb> =
+        &[sql! (
+            CREATE TABLE editors(
+                item_id INTEGER NOT NULL,
+                workspace_id INTEGER NOT NULL,
+                path BLOB NOT NULL,
+                PRIMARY KEY(item_id, workspace_id),
+                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                ON DELETE CASCADE
+                ON UPDATE CASCADE
+            ) STRICT;
+        ),
+        sql! (
+            ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
+            ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
+            ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
+        )];
+);
+
+impl EditorDb {
+    query! {
+        pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
+            SELECT path FROM editors
+            WHERE item_id = ? AND workspace_id = ?
+        }
+    }
+
+    query! {
+        pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> {
+            INSERT INTO editors
+                (item_id, workspace_id, path)
+            VALUES
+                (?1, ?2, ?3)
+            ON CONFLICT DO UPDATE SET
+                item_id = ?1,
+                workspace_id = ?2,
+                path = ?3
+        }
+    }
+
+    // Returns the scroll top row, and offset
+    query! {
+        pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
+            SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
+            FROM editors
+            WHERE item_id = ? AND workspace_id = ?
+        }
+    }
+
+    query! {
+        pub async fn save_scroll_position(
+            item_id: ItemId,
+            workspace_id: WorkspaceId,
+            top_row: u32,
+            vertical_offset: f32,
+            horizontal_offset: f32
+        ) -> Result<()> {
+            UPDATE OR IGNORE editors
+            SET
+                scroll_top_row = ?3,
+                scroll_horizontal_offset = ?4,
+                scroll_vertical_offset = ?5
+            WHERE item_id = ?1 AND workspace_id = ?2
+        }
+    }
+}

crates/editor2/src/scroll.rs ๐Ÿ”—

@@ -0,0 +1,436 @@
+pub mod actions;
+pub mod autoscroll;
+pub mod scroll_amount;
+
+use std::{
+    cmp::Ordering,
+    time::{Duration, Instant},
+};
+
+use gpui::{
+    geometry::vector::{vec2f, Vector2F},
+    AppContext, Axis, Task, ViewContext,
+};
+use language::{Bias, Point};
+use util::ResultExt;
+use workspace::WorkspaceId;
+
+use crate::{
+    display_map::{DisplaySnapshot, ToDisplayPoint},
+    hover_popover::hide_hover,
+    persistence::DB,
+    Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
+    ToPoint,
+};
+
+use self::{
+    autoscroll::{Autoscroll, AutoscrollStrategy},
+    scroll_amount::ScrollAmount,
+};
+
+pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
+pub const VERTICAL_SCROLL_MARGIN: f32 = 3.;
+const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
+
+#[derive(Default)]
+pub struct ScrollbarAutoHide(pub bool);
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct ScrollAnchor {
+    pub offset: Vector2F,
+    pub anchor: Anchor,
+}
+
+impl ScrollAnchor {
+    fn new() -> Self {
+        Self {
+            offset: Vector2F::zero(),
+            anchor: Anchor::min(),
+        }
+    }
+
+    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
+        let mut scroll_position = self.offset;
+        if self.anchor != Anchor::min() {
+            let scroll_top = self.anchor.to_display_point(snapshot).row() as f32;
+            scroll_position.set_y(scroll_top + scroll_position.y());
+        } else {
+            scroll_position.set_y(0.);
+        }
+        scroll_position
+    }
+
+    pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
+        self.anchor.to_point(buffer).row
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct OngoingScroll {
+    last_event: Instant,
+    axis: Option<Axis>,
+}
+
+impl OngoingScroll {
+    fn new() -> Self {
+        Self {
+            last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
+            axis: None,
+        }
+    }
+
+    pub fn filter(&self, delta: &mut Vector2F) -> Option<Axis> {
+        const UNLOCK_PERCENT: f32 = 1.9;
+        const UNLOCK_LOWER_BOUND: f32 = 6.;
+        let mut axis = self.axis;
+
+        let x = delta.x().abs();
+        let y = delta.y().abs();
+        let duration = Instant::now().duration_since(self.last_event);
+        if duration > SCROLL_EVENT_SEPARATION {
+            //New ongoing scroll will start, determine axis
+            axis = if x <= y {
+                Some(Axis::Vertical)
+            } else {
+                Some(Axis::Horizontal)
+            };
+        } else if x.max(y) >= UNLOCK_LOWER_BOUND {
+            //Check if the current ongoing will need to unlock
+            match axis {
+                Some(Axis::Vertical) => {
+                    if x > y && x >= y * UNLOCK_PERCENT {
+                        axis = None;
+                    }
+                }
+
+                Some(Axis::Horizontal) => {
+                    if y > x && y >= x * UNLOCK_PERCENT {
+                        axis = None;
+                    }
+                }
+
+                None => {}
+            }
+        }
+
+        match axis {
+            Some(Axis::Vertical) => *delta = vec2f(0., delta.y()),
+            Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.),
+            None => {}
+        }
+
+        axis
+    }
+}
+
+pub struct ScrollManager {
+    vertical_scroll_margin: f32,
+    anchor: ScrollAnchor,
+    ongoing: OngoingScroll,
+    autoscroll_request: Option<(Autoscroll, bool)>,
+    last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>,
+    show_scrollbars: bool,
+    hide_scrollbar_task: Option<Task<()>>,
+    visible_line_count: Option<f32>,
+}
+
+impl ScrollManager {
+    pub fn new() -> Self {
+        ScrollManager {
+            vertical_scroll_margin: VERTICAL_SCROLL_MARGIN,
+            anchor: ScrollAnchor::new(),
+            ongoing: OngoingScroll::new(),
+            autoscroll_request: None,
+            show_scrollbars: true,
+            hide_scrollbar_task: None,
+            last_autoscroll: None,
+            visible_line_count: None,
+        }
+    }
+
+    pub fn clone_state(&mut self, other: &Self) {
+        self.anchor = other.anchor;
+        self.ongoing = other.ongoing;
+    }
+
+    pub fn anchor(&self) -> ScrollAnchor {
+        self.anchor
+    }
+
+    pub fn ongoing_scroll(&self) -> OngoingScroll {
+        self.ongoing
+    }
+
+    pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
+        self.ongoing.last_event = Instant::now();
+        self.ongoing.axis = axis;
+    }
+
+    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
+        self.anchor.scroll_position(snapshot)
+    }
+
+    fn set_scroll_position(
+        &mut self,
+        scroll_position: Vector2F,
+        map: &DisplaySnapshot,
+        local: bool,
+        autoscroll: bool,
+        workspace_id: Option<i64>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let (new_anchor, top_row) = if scroll_position.y() <= 0. {
+            (
+                ScrollAnchor {
+                    anchor: Anchor::min(),
+                    offset: scroll_position.max(vec2f(0., 0.)),
+                },
+                0,
+            )
+        } else {
+            let scroll_top_buffer_point =
+                DisplayPoint::new(scroll_position.y() as u32, 0).to_point(&map);
+            let top_anchor = map
+                .buffer_snapshot
+                .anchor_at(scroll_top_buffer_point, Bias::Right);
+
+            (
+                ScrollAnchor {
+                    anchor: top_anchor,
+                    offset: vec2f(
+                        scroll_position.x(),
+                        scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
+                    ),
+                },
+                scroll_top_buffer_point.row,
+            )
+        };
+
+        self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx);
+    }
+
+    fn set_anchor(
+        &mut self,
+        anchor: ScrollAnchor,
+        top_row: u32,
+        local: bool,
+        autoscroll: bool,
+        workspace_id: Option<i64>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        self.anchor = anchor;
+        cx.emit(Event::ScrollPositionChanged { local, autoscroll });
+        self.show_scrollbar(cx);
+        self.autoscroll_request.take();
+        if let Some(workspace_id) = workspace_id {
+            let item_id = cx.view_id();
+
+            cx.background()
+                .spawn(async move {
+                    DB.save_scroll_position(
+                        item_id,
+                        workspace_id,
+                        top_row,
+                        anchor.offset.x(),
+                        anchor.offset.y(),
+                    )
+                    .await
+                    .log_err()
+                })
+                .detach()
+        }
+        cx.notify();
+    }
+
+    pub fn show_scrollbar(&mut self, cx: &mut ViewContext<Editor>) {
+        if !self.show_scrollbars {
+            self.show_scrollbars = true;
+            cx.notify();
+        }
+
+        if cx.default_global::<ScrollbarAutoHide>().0 {
+            self.hide_scrollbar_task = Some(cx.spawn(|editor, mut cx| async move {
+                cx.background().timer(SCROLLBAR_SHOW_INTERVAL).await;
+                editor
+                    .update(&mut cx, |editor, cx| {
+                        editor.scroll_manager.show_scrollbars = false;
+                        cx.notify();
+                    })
+                    .log_err();
+            }));
+        } else {
+            self.hide_scrollbar_task = None;
+        }
+    }
+
+    pub fn scrollbars_visible(&self) -> bool {
+        self.show_scrollbars
+    }
+
+    pub fn has_autoscroll_request(&self) -> bool {
+        self.autoscroll_request.is_some()
+    }
+
+    pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
+        if max < self.anchor.offset.x() {
+            self.anchor.offset.set_x(max);
+            true
+        } else {
+            false
+        }
+    }
+}
+
+impl Editor {
+    pub fn vertical_scroll_margin(&mut self) -> usize {
+        self.scroll_manager.vertical_scroll_margin as usize
+    }
+
+    pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext<Self>) {
+        self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
+        cx.notify();
+    }
+
+    pub fn visible_line_count(&self) -> Option<f32> {
+        self.scroll_manager.visible_line_count
+    }
+
+    pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
+        let opened_first_time = self.scroll_manager.visible_line_count.is_none();
+        self.scroll_manager.visible_line_count = Some(lines);
+        if opened_first_time {
+            cx.spawn(|editor, mut cx| async move {
+                editor
+                    .update(&mut cx, |editor, cx| {
+                        editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
+                    })
+                    .ok()
+            })
+            .detach()
+        }
+    }
+
+    pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
+        self.set_scroll_position_internal(scroll_position, true, false, cx);
+    }
+
+    pub(crate) fn set_scroll_position_internal(
+        &mut self,
+        scroll_position: Vector2F,
+        local: bool,
+        autoscroll: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        hide_hover(self, cx);
+        let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
+        self.scroll_manager.set_scroll_position(
+            scroll_position,
+            &map,
+            local,
+            autoscroll,
+            workspace_id,
+            cx,
+        );
+
+        self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+    }
+
+    pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        self.scroll_manager.anchor.scroll_position(&display_map)
+    }
+
+    pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
+        hide_hover(self, cx);
+        let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
+        let top_row = scroll_anchor
+            .anchor
+            .to_point(&self.buffer().read(cx).snapshot(cx))
+            .row;
+        self.scroll_manager
+            .set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx);
+    }
+
+    pub(crate) fn set_scroll_anchor_remote(
+        &mut self,
+        scroll_anchor: ScrollAnchor,
+        cx: &mut ViewContext<Self>,
+    ) {
+        hide_hover(self, cx);
+        let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
+        let top_row = scroll_anchor
+            .anchor
+            .to_point(&self.buffer().read(cx).snapshot(cx))
+            .row;
+        self.scroll_manager
+            .set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
+    }
+
+    pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        if self.take_rename(true, cx).is_some() {
+            return;
+        }
+
+        let cur_position = self.scroll_position(cx);
+        let new_pos = cur_position + vec2f(0., amount.lines(self));
+        self.set_scroll_position(new_pos, cx);
+    }
+
+    /// Returns an ordering. The newest selection is:
+    ///     Ordering::Equal => on screen
+    ///     Ordering::Less => above the screen
+    ///     Ordering::Greater => below the screen
+    pub fn newest_selection_on_screen(&self, cx: &mut AppContext) -> Ordering {
+        let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let newest_head = self
+            .selections
+            .newest_anchor()
+            .head()
+            .to_display_point(&snapshot);
+        let screen_top = self
+            .scroll_manager
+            .anchor
+            .anchor
+            .to_display_point(&snapshot);
+
+        if screen_top > newest_head {
+            return Ordering::Less;
+        }
+
+        if let Some(visible_lines) = self.visible_line_count() {
+            if newest_head.row() < screen_top.row() + visible_lines as u32 {
+                return Ordering::Equal;
+            }
+        }
+
+        Ordering::Greater
+    }
+
+    pub fn read_scroll_position_from_db(
+        &mut self,
+        item_id: usize,
+        workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let scroll_position = DB.get_scroll_position(item_id, workspace_id);
+        if let Ok(Some((top_row, x, y))) = scroll_position {
+            let top_anchor = self
+                .buffer()
+                .read(cx)
+                .snapshot(cx)
+                .anchor_at(Point::new(top_row as u32, 0), Bias::Left);
+            let scroll_anchor = ScrollAnchor {
+                offset: Vector2F::new(x, y),
+                anchor: top_anchor,
+            };
+            self.set_scroll_anchor(scroll_anchor, cx);
+        }
+    }
+}

crates/editor2/src/scroll/actions.rs ๐Ÿ”—

@@ -0,0 +1,152 @@
+use gpui::{actions, geometry::vector::Vector2F, AppContext, Axis, ViewContext};
+use language::Bias;
+
+use crate::{Editor, EditorMode};
+
+use super::{autoscroll::Autoscroll, scroll_amount::ScrollAmount, ScrollAnchor};
+
+actions!(
+    editor,
+    [
+        LineDown,
+        LineUp,
+        HalfPageDown,
+        HalfPageUp,
+        PageDown,
+        PageUp,
+        NextScreen,
+        ScrollCursorTop,
+        ScrollCursorCenter,
+        ScrollCursorBottom,
+    ]
+);
+
+pub fn init(cx: &mut AppContext) {
+    cx.add_action(Editor::next_screen);
+    cx.add_action(Editor::scroll_cursor_top);
+    cx.add_action(Editor::scroll_cursor_center);
+    cx.add_action(Editor::scroll_cursor_bottom);
+    cx.add_action(|this: &mut Editor, _: &LineDown, cx| {
+        this.scroll_screen(&ScrollAmount::Line(1.), cx)
+    });
+    cx.add_action(|this: &mut Editor, _: &LineUp, cx| {
+        this.scroll_screen(&ScrollAmount::Line(-1.), cx)
+    });
+    cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| {
+        this.scroll_screen(&ScrollAmount::Page(0.5), cx)
+    });
+    cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| {
+        this.scroll_screen(&ScrollAmount::Page(-0.5), cx)
+    });
+    cx.add_action(|this: &mut Editor, _: &PageDown, cx| {
+        this.scroll_screen(&ScrollAmount::Page(1.), cx)
+    });
+    cx.add_action(|this: &mut Editor, _: &PageUp, cx| {
+        this.scroll_screen(&ScrollAmount::Page(-1.), cx)
+    });
+}
+
+impl Editor {
+    pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) -> Option<()> {
+        if self.take_rename(true, cx).is_some() {
+            return None;
+        }
+
+        if self.mouse_context_menu.read(cx).visible() {
+            return None;
+        }
+
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return None;
+        }
+        self.request_autoscroll(Autoscroll::Next, cx);
+        Some(())
+    }
+
+    pub fn scroll(
+        &mut self,
+        scroll_position: Vector2F,
+        axis: Option<Axis>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.scroll_manager.update_ongoing_scroll(axis);
+        self.set_scroll_position(scroll_position, cx);
+    }
+
+    fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {
+        let snapshot = editor.snapshot(cx).display_snapshot;
+        let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
+
+        let mut new_screen_top = editor.selections.newest_display(cx).head();
+        *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(scroll_margin_rows);
+        *new_screen_top.column_mut() = 0;
+        let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
+        let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
+
+        editor.set_scroll_anchor(
+            ScrollAnchor {
+                anchor: new_anchor,
+                offset: Default::default(),
+            },
+            cx,
+        )
+    }
+
+    fn scroll_cursor_center(
+        editor: &mut Editor,
+        _: &ScrollCursorCenter,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let snapshot = editor.snapshot(cx).display_snapshot;
+        let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
+            visible_rows as u32
+        } else {
+            return;
+        };
+
+        let mut new_screen_top = editor.selections.newest_display(cx).head();
+        *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(visible_rows / 2);
+        *new_screen_top.column_mut() = 0;
+        let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
+        let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
+
+        editor.set_scroll_anchor(
+            ScrollAnchor {
+                anchor: new_anchor,
+                offset: Default::default(),
+            },
+            cx,
+        )
+    }
+
+    fn scroll_cursor_bottom(
+        editor: &mut Editor,
+        _: &ScrollCursorBottom,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let snapshot = editor.snapshot(cx).display_snapshot;
+        let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
+        let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
+            visible_rows as u32
+        } else {
+            return;
+        };
+
+        let mut new_screen_top = editor.selections.newest_display(cx).head();
+        *new_screen_top.row_mut() = new_screen_top
+            .row()
+            .saturating_sub(visible_rows.saturating_sub(scroll_margin_rows));
+        *new_screen_top.column_mut() = 0;
+        let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
+        let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
+
+        editor.set_scroll_anchor(
+            ScrollAnchor {
+                anchor: new_anchor,
+                offset: Default::default(),
+            },
+            cx,
+        )
+    }
+}

crates/editor2/src/scroll/autoscroll.rs ๐Ÿ”—

@@ -0,0 +1,258 @@
+use std::cmp;
+
+use gpui::ViewContext;
+use language::Point;
+
+use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
+
+#[derive(PartialEq, Eq)]
+pub enum Autoscroll {
+    Next,
+    Strategy(AutoscrollStrategy),
+}
+
+impl Autoscroll {
+    pub fn fit() -> Self {
+        Self::Strategy(AutoscrollStrategy::Fit)
+    }
+
+    pub fn newest() -> Self {
+        Self::Strategy(AutoscrollStrategy::Newest)
+    }
+
+    pub fn center() -> Self {
+        Self::Strategy(AutoscrollStrategy::Center)
+    }
+}
+
+#[derive(PartialEq, Eq, Default)]
+pub enum AutoscrollStrategy {
+    Fit,
+    Newest,
+    #[default]
+    Center,
+    Top,
+    Bottom,
+}
+
+impl AutoscrollStrategy {
+    fn next(&self) -> Self {
+        match self {
+            AutoscrollStrategy::Center => AutoscrollStrategy::Top,
+            AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
+            _ => AutoscrollStrategy::Center,
+        }
+    }
+}
+
+impl Editor {
+    pub fn autoscroll_vertically(
+        &mut self,
+        viewport_height: f32,
+        line_height: f32,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        let visible_lines = viewport_height / line_height;
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
+        let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
+            (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
+        } else {
+            display_map.max_point().row() as f32
+        };
+        if scroll_position.y() > max_scroll_top {
+            scroll_position.set_y(max_scroll_top);
+            self.set_scroll_position(scroll_position, cx);
+        }
+
+        let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
+            return false;
+        };
+
+        let mut target_top;
+        let mut target_bottom;
+        if let Some(highlighted_rows) = &self.highlighted_rows {
+            target_top = highlighted_rows.start as f32;
+            target_bottom = target_top + 1.;
+        } else {
+            let selections = self.selections.all::<Point>(cx);
+            target_top = selections
+                .first()
+                .unwrap()
+                .head()
+                .to_display_point(&display_map)
+                .row() as f32;
+            target_bottom = selections
+                .last()
+                .unwrap()
+                .head()
+                .to_display_point(&display_map)
+                .row() as f32
+                + 1.0;
+
+            // If the selections can't all fit on screen, scroll to the newest.
+            if autoscroll == Autoscroll::newest()
+                || autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines
+            {
+                let newest_selection_top = selections
+                    .iter()
+                    .max_by_key(|s| s.id)
+                    .unwrap()
+                    .head()
+                    .to_display_point(&display_map)
+                    .row() as f32;
+                target_top = newest_selection_top;
+                target_bottom = newest_selection_top + 1.;
+            }
+        }
+
+        let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
+            0.
+        } else {
+            ((visible_lines - (target_bottom - target_top)) / 2.0).floor()
+        };
+
+        let strategy = match autoscroll {
+            Autoscroll::Strategy(strategy) => strategy,
+            Autoscroll::Next => {
+                let last_autoscroll = &self.scroll_manager.last_autoscroll;
+                if let Some(last_autoscroll) = last_autoscroll {
+                    if self.scroll_manager.anchor.offset == last_autoscroll.0
+                        && target_top == last_autoscroll.1
+                        && target_bottom == last_autoscroll.2
+                    {
+                        last_autoscroll.3.next()
+                    } else {
+                        AutoscrollStrategy::default()
+                    }
+                } else {
+                    AutoscrollStrategy::default()
+                }
+            }
+        };
+
+        match strategy {
+            AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
+                let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
+                let target_top = (target_top - margin).max(0.0);
+                let target_bottom = target_bottom + margin;
+                let start_row = scroll_position.y();
+                let end_row = start_row + visible_lines;
+
+                let needs_scroll_up = target_top < start_row;
+                let needs_scroll_down = target_bottom >= end_row;
+
+                if needs_scroll_up && !needs_scroll_down {
+                    scroll_position.set_y(target_top);
+                    self.set_scroll_position_internal(scroll_position, local, true, cx);
+                }
+                if !needs_scroll_up && needs_scroll_down {
+                    scroll_position.set_y(target_bottom - visible_lines);
+                    self.set_scroll_position_internal(scroll_position, local, true, cx);
+                }
+            }
+            AutoscrollStrategy::Center => {
+                scroll_position.set_y((target_top - margin).max(0.0));
+                self.set_scroll_position_internal(scroll_position, local, true, cx);
+            }
+            AutoscrollStrategy::Top => {
+                scroll_position.set_y((target_top).max(0.0));
+                self.set_scroll_position_internal(scroll_position, local, true, cx);
+            }
+            AutoscrollStrategy::Bottom => {
+                scroll_position.set_y((target_bottom - visible_lines).max(0.0));
+                self.set_scroll_position_internal(scroll_position, local, true, cx);
+            }
+        }
+
+        self.scroll_manager.last_autoscroll = Some((
+            self.scroll_manager.anchor.offset,
+            target_top,
+            target_bottom,
+            strategy,
+        ));
+
+        true
+    }
+
+    pub fn autoscroll_horizontally(
+        &mut self,
+        start_row: u32,
+        viewport_width: f32,
+        scroll_width: f32,
+        max_glyph_width: f32,
+        layouts: &[LineWithInvisibles],
+        cx: &mut ViewContext<Self>,
+    ) -> bool {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let selections = self.selections.all::<Point>(cx);
+
+        let mut target_left;
+        let mut target_right;
+
+        if self.highlighted_rows.is_some() {
+            target_left = 0.0_f32;
+            target_right = 0.0_f32;
+        } else {
+            target_left = std::f32::INFINITY;
+            target_right = 0.0_f32;
+            for selection in selections {
+                let head = selection.head().to_display_point(&display_map);
+                if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 {
+                    let start_column = head.column().saturating_sub(3);
+                    let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
+                    target_left = target_left.min(
+                        layouts[(head.row() - start_row) as usize]
+                            .line
+                            .x_for_index(start_column as usize),
+                    );
+                    target_right = target_right.max(
+                        layouts[(head.row() - start_row) as usize]
+                            .line
+                            .x_for_index(end_column as usize)
+                            + max_glyph_width,
+                    );
+                }
+            }
+        }
+
+        target_right = target_right.min(scroll_width);
+
+        if target_right - target_left > viewport_width {
+            return false;
+        }
+
+        let scroll_left = self.scroll_manager.anchor.offset.x() * max_glyph_width;
+        let scroll_right = scroll_left + viewport_width;
+
+        if target_left < scroll_left {
+            self.scroll_manager
+                .anchor
+                .offset
+                .set_x(target_left / max_glyph_width);
+            true
+        } else if target_right > scroll_right {
+            self.scroll_manager
+                .anchor
+                .offset
+                .set_x((target_right - viewport_width) / max_glyph_width);
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
+        self.scroll_manager.autoscroll_request = Some((autoscroll, true));
+        cx.notify();
+    }
+
+    pub(crate) fn request_autoscroll_remotely(
+        &mut self,
+        autoscroll: Autoscroll,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.scroll_manager.autoscroll_request = Some((autoscroll, false));
+        cx.notify();
+    }
+}

crates/editor2/src/scroll/scroll_amount.rs ๐Ÿ”—

@@ -0,0 +1,28 @@
+use crate::Editor;
+use serde::Deserialize;
+
+#[derive(Clone, PartialEq, Deserialize)]
+pub enum ScrollAmount {
+    // Scroll N lines (positive is towards the end of the document)
+    Line(f32),
+    // Scroll N pages (positive is towards the end of the document)
+    Page(f32),
+}
+
+impl ScrollAmount {
+    pub fn lines(&self, editor: &mut Editor) -> f32 {
+        match self {
+            Self::Line(count) => *count,
+            Self::Page(count) => editor
+                .visible_line_count()
+                .map(|mut l| {
+                    // for full pages subtract one to leave an anchor line
+                    if count.abs() == 1.0 {
+                        l -= 1.0
+                    }
+                    (l * count).trunc()
+                })
+                .unwrap_or(0.),
+        }
+    }
+}

crates/editor2/src/selections_collection.rs ๐Ÿ”—

@@ -0,0 +1,886 @@
+use std::{
+    cell::Ref,
+    iter, mem,
+    ops::{Deref, DerefMut, Range, Sub},
+    sync::Arc,
+};
+
+use collections::HashMap;
+use gpui::{AppContext, ModelHandle};
+use itertools::Itertools;
+use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
+use util::post_inc;
+
+use crate::{
+    display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+    movement::TextLayoutDetails,
+    Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
+};
+
+#[derive(Debug, Clone)]
+pub struct PendingSelection {
+    pub selection: Selection<Anchor>,
+    pub mode: SelectMode,
+}
+
+#[derive(Debug, Clone)]
+pub struct SelectionsCollection {
+    display_map: ModelHandle<DisplayMap>,
+    buffer: ModelHandle<MultiBuffer>,
+    pub next_selection_id: usize,
+    pub line_mode: bool,
+    disjoint: Arc<[Selection<Anchor>]>,
+    pending: Option<PendingSelection>,
+}
+
+impl SelectionsCollection {
+    pub fn new(display_map: ModelHandle<DisplayMap>, buffer: ModelHandle<MultiBuffer>) -> Self {
+        Self {
+            display_map,
+            buffer,
+            next_selection_id: 1,
+            line_mode: false,
+            disjoint: Arc::from([]),
+            pending: Some(PendingSelection {
+                selection: Selection {
+                    id: 0,
+                    start: Anchor::min(),
+                    end: Anchor::min(),
+                    reversed: false,
+                    goal: SelectionGoal::None,
+                },
+                mode: SelectMode::Character,
+            }),
+        }
+    }
+
+    pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot {
+        self.display_map.update(cx, |map, cx| map.snapshot(cx))
+    }
+
+    fn buffer<'a>(&self, cx: &'a AppContext) -> Ref<'a, MultiBufferSnapshot> {
+        self.buffer.read(cx).read(cx)
+    }
+
+    pub fn clone_state(&mut self, other: &SelectionsCollection) {
+        self.next_selection_id = other.next_selection_id;
+        self.line_mode = other.line_mode;
+        self.disjoint = other.disjoint.clone();
+        self.pending = other.pending.clone();
+    }
+
+    pub fn count(&self) -> usize {
+        let mut count = self.disjoint.len();
+        if self.pending.is_some() {
+            count += 1;
+        }
+        count
+    }
+
+    /// The non-pending, non-overlapping selections. There could still be a pending
+    /// selection that overlaps these if the mouse is being dragged, etc. Returned as
+    /// selections over Anchors.
+    pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
+        self.disjoint.clone()
+    }
+
+    pub fn pending_anchor(&self) -> Option<Selection<Anchor>> {
+        self.pending
+            .as_ref()
+            .map(|pending| pending.selection.clone())
+    }
+
+    pub fn pending<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &AppContext,
+    ) -> Option<Selection<D>> {
+        self.pending_anchor()
+            .as_ref()
+            .map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
+    }
+
+    pub fn pending_mode(&self) -> Option<SelectMode> {
+        self.pending.as_ref().map(|pending| pending.mode.clone())
+    }
+
+    pub fn all<'a, D>(&self, cx: &AppContext) -> Vec<Selection<D>>
+    where
+        D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
+    {
+        let disjoint_anchors = &self.disjoint;
+        let mut disjoint =
+            resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
+
+        let mut pending_opt = self.pending::<D>(cx);
+
+        iter::from_fn(move || {
+            if let Some(pending) = pending_opt.as_mut() {
+                while let Some(next_selection) = disjoint.peek() {
+                    if pending.start <= next_selection.end && pending.end >= next_selection.start {
+                        let next_selection = disjoint.next().unwrap();
+                        if next_selection.start < pending.start {
+                            pending.start = next_selection.start;
+                        }
+                        if next_selection.end > pending.end {
+                            pending.end = next_selection.end;
+                        }
+                    } else if next_selection.end < pending.start {
+                        return disjoint.next();
+                    } else {
+                        break;
+                    }
+                }
+
+                pending_opt.take()
+            } else {
+                disjoint.next()
+            }
+        })
+        .collect()
+    }
+
+    /// Returns all of the selections, adjusted to take into account the selection line_mode
+    pub fn all_adjusted(&self, cx: &mut AppContext) -> Vec<Selection<Point>> {
+        let mut selections = self.all::<Point>(cx);
+        if self.line_mode {
+            let map = self.display_map(cx);
+            for selection in &mut selections {
+                let new_range = map.expand_to_line(selection.range());
+                selection.start = new_range.start;
+                selection.end = new_range.end;
+            }
+        }
+        selections
+    }
+
+    pub fn all_adjusted_display(
+        &self,
+        cx: &mut AppContext,
+    ) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
+        if self.line_mode {
+            let selections = self.all::<Point>(cx);
+            let map = self.display_map(cx);
+            let result = selections
+                .into_iter()
+                .map(|mut selection| {
+                    let new_range = map.expand_to_line(selection.range());
+                    selection.start = new_range.start;
+                    selection.end = new_range.end;
+                    selection.map(|point| point.to_display_point(&map))
+                })
+                .collect();
+            (map, result)
+        } else {
+            self.all_display(cx)
+        }
+    }
+
+    pub fn disjoint_in_range<'a, D>(
+        &self,
+        range: Range<Anchor>,
+        cx: &AppContext,
+    ) -> Vec<Selection<D>>
+    where
+        D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
+    {
+        let buffer = self.buffer(cx);
+        let start_ix = match self
+            .disjoint
+            .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer))
+        {
+            Ok(ix) | Err(ix) => ix,
+        };
+        let end_ix = match self
+            .disjoint
+            .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer))
+        {
+            Ok(ix) => ix + 1,
+            Err(ix) => ix,
+        };
+        resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect()
+    }
+
+    pub fn all_display(
+        &self,
+        cx: &mut AppContext,
+    ) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
+        let display_map = self.display_map(cx);
+        let selections = self
+            .all::<Point>(cx)
+            .into_iter()
+            .map(|selection| selection.map(|point| point.to_display_point(&display_map)))
+            .collect();
+        (display_map, selections)
+    }
+
+    pub fn newest_anchor(&self) -> &Selection<Anchor> {
+        self.pending
+            .as_ref()
+            .map(|s| &s.selection)
+            .or_else(|| self.disjoint.iter().max_by_key(|s| s.id))
+            .unwrap()
+    }
+
+    pub fn newest<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &AppContext,
+    ) -> Selection<D> {
+        resolve(self.newest_anchor(), &self.buffer(cx))
+    }
+
+    pub fn newest_display(&self, cx: &mut AppContext) -> Selection<DisplayPoint> {
+        let display_map = self.display_map(cx);
+        let selection = self
+            .newest_anchor()
+            .map(|point| point.to_display_point(&display_map));
+        selection
+    }
+
+    pub fn oldest_anchor(&self) -> &Selection<Anchor> {
+        self.disjoint
+            .iter()
+            .min_by_key(|s| s.id)
+            .or_else(|| self.pending.as_ref().map(|p| &p.selection))
+            .unwrap()
+    }
+
+    pub fn oldest<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &AppContext,
+    ) -> Selection<D> {
+        resolve(self.oldest_anchor(), &self.buffer(cx))
+    }
+
+    pub fn first_anchor(&self) -> Selection<Anchor> {
+        self.disjoint[0].clone()
+    }
+
+    pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &AppContext,
+    ) -> Selection<D> {
+        self.all(cx).first().unwrap().clone()
+    }
+
+    pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &AppContext,
+    ) -> Selection<D> {
+        self.all(cx).last().unwrap().clone()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
+        &self,
+        cx: &AppContext,
+    ) -> Vec<Range<D>> {
+        self.all::<D>(cx)
+            .iter()
+            .map(|s| {
+                if s.reversed {
+                    s.end.clone()..s.start.clone()
+                } else {
+                    s.start.clone()..s.end.clone()
+                }
+            })
+            .collect()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn display_ranges(&self, cx: &mut AppContext) -> Vec<Range<DisplayPoint>> {
+        let display_map = self.display_map(cx);
+        self.disjoint_anchors()
+            .iter()
+            .chain(self.pending_anchor().as_ref())
+            .map(|s| {
+                if s.reversed {
+                    s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
+                } else {
+                    s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
+                }
+            })
+            .collect()
+    }
+
+    pub fn build_columnar_selection(
+        &mut self,
+        display_map: &DisplaySnapshot,
+        row: u32,
+        positions: &Range<f32>,
+        reversed: bool,
+        text_layout_details: &TextLayoutDetails,
+    ) -> Option<Selection<Point>> {
+        let is_empty = positions.start == positions.end;
+        let line_len = display_map.line_len(row);
+
+        let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
+
+        let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
+        if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) {
+            let start = DisplayPoint::new(row, start_col);
+            let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
+            let end = DisplayPoint::new(row, end_col);
+
+            Some(Selection {
+                id: post_inc(&mut self.next_selection_id),
+                start: start.to_point(display_map),
+                end: end.to_point(display_map),
+                reversed,
+                goal: SelectionGoal::HorizontalRange {
+                    start: positions.start,
+                    end: positions.end,
+                },
+            })
+        } else {
+            None
+        }
+    }
+
+    pub(crate) fn change_with<R>(
+        &mut self,
+        cx: &mut AppContext,
+        change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
+    ) -> (bool, R) {
+        let mut mutable_collection = MutableSelectionsCollection {
+            collection: self,
+            selections_changed: false,
+            cx,
+        };
+
+        let result = change(&mut mutable_collection);
+        assert!(
+            !mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(),
+            "There must be at least one selection"
+        );
+        (mutable_collection.selections_changed, result)
+    }
+}
+
+pub struct MutableSelectionsCollection<'a> {
+    collection: &'a mut SelectionsCollection,
+    selections_changed: bool,
+    cx: &'a mut AppContext,
+}
+
+impl<'a> MutableSelectionsCollection<'a> {
+    pub fn display_map(&mut self) -> DisplaySnapshot {
+        self.collection.display_map(self.cx)
+    }
+
+    fn buffer(&self) -> Ref<MultiBufferSnapshot> {
+        self.collection.buffer(self.cx)
+    }
+
+    pub fn clear_disjoint(&mut self) {
+        self.collection.disjoint = Arc::from([]);
+    }
+
+    pub fn delete(&mut self, selection_id: usize) {
+        let mut changed = false;
+        self.collection.disjoint = self
+            .disjoint
+            .iter()
+            .filter(|selection| {
+                let found = selection.id == selection_id;
+                changed |= found;
+                !found
+            })
+            .cloned()
+            .collect();
+
+        self.selections_changed |= changed;
+    }
+
+    pub fn clear_pending(&mut self) {
+        if self.collection.pending.is_some() {
+            self.collection.pending = None;
+            self.selections_changed = true;
+        }
+    }
+
+    pub fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
+        self.collection.pending = Some(PendingSelection {
+            selection: Selection {
+                id: post_inc(&mut self.collection.next_selection_id),
+                start: range.start,
+                end: range.end,
+                reversed: false,
+                goal: SelectionGoal::None,
+            },
+            mode,
+        });
+        self.selections_changed = true;
+    }
+
+    pub fn set_pending_display_range(&mut self, range: Range<DisplayPoint>, mode: SelectMode) {
+        let (start, end, reversed) = {
+            let display_map = self.display_map();
+            let buffer = self.buffer();
+            let mut start = range.start;
+            let mut end = range.end;
+            let reversed = if start > end {
+                mem::swap(&mut start, &mut end);
+                true
+            } else {
+                false
+            };
+
+            let end_bias = if end > start { Bias::Left } else { Bias::Right };
+            (
+                buffer.anchor_before(start.to_point(&display_map)),
+                buffer.anchor_at(end.to_point(&display_map), end_bias),
+                reversed,
+            )
+        };
+
+        let new_pending = PendingSelection {
+            selection: Selection {
+                id: post_inc(&mut self.collection.next_selection_id),
+                start,
+                end,
+                reversed,
+                goal: SelectionGoal::None,
+            },
+            mode,
+        };
+
+        self.collection.pending = Some(new_pending);
+        self.selections_changed = true;
+    }
+
+    pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
+        self.collection.pending = Some(PendingSelection { selection, mode });
+        self.selections_changed = true;
+    }
+
+    pub fn try_cancel(&mut self) -> bool {
+        if let Some(pending) = self.collection.pending.take() {
+            if self.disjoint.is_empty() {
+                self.collection.disjoint = Arc::from([pending.selection]);
+            }
+            self.selections_changed = true;
+            return true;
+        }
+
+        let mut oldest = self.oldest_anchor().clone();
+        if self.count() > 1 {
+            self.collection.disjoint = Arc::from([oldest]);
+            self.selections_changed = true;
+            return true;
+        }
+
+        if !oldest.start.cmp(&oldest.end, &self.buffer()).is_eq() {
+            let head = oldest.head();
+            oldest.start = head.clone();
+            oldest.end = head;
+            self.collection.disjoint = Arc::from([oldest]);
+            self.selections_changed = true;
+            return true;
+        }
+
+        false
+    }
+
+    pub fn insert_range<T>(&mut self, range: Range<T>)
+    where
+        T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
+    {
+        let mut selections = self.all(self.cx);
+        let mut start = range.start.to_offset(&self.buffer());
+        let mut end = range.end.to_offset(&self.buffer());
+        let reversed = if start > end {
+            mem::swap(&mut start, &mut end);
+            true
+        } else {
+            false
+        };
+        selections.push(Selection {
+            id: post_inc(&mut self.collection.next_selection_id),
+            start,
+            end,
+            reversed,
+            goal: SelectionGoal::None,
+        });
+        self.select(selections);
+    }
+
+    pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
+    where
+        T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
+    {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        selections.sort_unstable_by_key(|s| s.start);
+        // Merge overlapping selections.
+        let mut i = 1;
+        while i < selections.len() {
+            if selections[i - 1].end >= selections[i].start {
+                let removed = selections.remove(i);
+                if removed.start < selections[i - 1].start {
+                    selections[i - 1].start = removed.start;
+                }
+                if removed.end > selections[i - 1].end {
+                    selections[i - 1].end = removed.end;
+                }
+            } else {
+                i += 1;
+            }
+        }
+
+        self.collection.disjoint = Arc::from_iter(selections.into_iter().map(|selection| {
+            let end_bias = if selection.end > selection.start {
+                Bias::Left
+            } else {
+                Bias::Right
+            };
+            Selection {
+                id: selection.id,
+                start: buffer.anchor_after(selection.start),
+                end: buffer.anchor_at(selection.end, end_bias),
+                reversed: selection.reversed,
+                goal: selection.goal,
+            }
+        }));
+
+        self.collection.pending = None;
+        self.selections_changed = true;
+    }
+
+    pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        let resolved_selections =
+            resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
+        self.select(resolved_selections);
+    }
+
+    pub fn select_ranges<I, T>(&mut self, ranges: I)
+    where
+        I: IntoIterator<Item = Range<T>>,
+        T: ToOffset,
+    {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        let ranges = ranges
+            .into_iter()
+            .map(|range| range.start.to_offset(&buffer)..range.end.to_offset(&buffer));
+        self.select_offset_ranges(ranges);
+    }
+
+    fn select_offset_ranges<I>(&mut self, ranges: I)
+    where
+        I: IntoIterator<Item = Range<usize>>,
+    {
+        let selections = ranges
+            .into_iter()
+            .map(|range| {
+                let mut start = range.start;
+                let mut end = range.end;
+                let reversed = if start > end {
+                    mem::swap(&mut start, &mut end);
+                    true
+                } else {
+                    false
+                };
+                Selection {
+                    id: post_inc(&mut self.collection.next_selection_id),
+                    start,
+                    end,
+                    reversed,
+                    goal: SelectionGoal::None,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        self.select(selections)
+    }
+
+    pub fn select_anchor_ranges<I: IntoIterator<Item = Range<Anchor>>>(&mut self, ranges: I) {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        let selections = ranges
+            .into_iter()
+            .map(|range| {
+                let mut start = range.start;
+                let mut end = range.end;
+                let reversed = if start.cmp(&end, &buffer).is_gt() {
+                    mem::swap(&mut start, &mut end);
+                    true
+                } else {
+                    false
+                };
+                Selection {
+                    id: post_inc(&mut self.collection.next_selection_id),
+                    start,
+                    end,
+                    reversed,
+                    goal: SelectionGoal::None,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        self.select_anchors(selections)
+    }
+
+    pub fn new_selection_id(&mut self) -> usize {
+        post_inc(&mut self.next_selection_id)
+    }
+
+    pub fn select_display_ranges<T>(&mut self, ranges: T)
+    where
+        T: IntoIterator<Item = Range<DisplayPoint>>,
+    {
+        let display_map = self.display_map();
+        let selections = ranges
+            .into_iter()
+            .map(|range| {
+                let mut start = range.start;
+                let mut end = range.end;
+                let reversed = if start > end {
+                    mem::swap(&mut start, &mut end);
+                    true
+                } else {
+                    false
+                };
+                Selection {
+                    id: post_inc(&mut self.collection.next_selection_id),
+                    start: start.to_point(&display_map),
+                    end: end.to_point(&display_map),
+                    reversed,
+                    goal: SelectionGoal::None,
+                }
+            })
+            .collect();
+        self.select(selections);
+    }
+
+    pub fn move_with(
+        &mut self,
+        mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
+    ) {
+        let mut changed = false;
+        let display_map = self.display_map();
+        let selections = self
+            .all::<Point>(self.cx)
+            .into_iter()
+            .map(|selection| {
+                let mut moved_selection =
+                    selection.map(|point| point.to_display_point(&display_map));
+                move_selection(&display_map, &mut moved_selection);
+                let moved_selection =
+                    moved_selection.map(|display_point| display_point.to_point(&display_map));
+                if selection != moved_selection {
+                    changed = true;
+                }
+                moved_selection
+            })
+            .collect();
+
+        if changed {
+            self.select(selections)
+        }
+    }
+
+    pub fn move_offsets_with(
+        &mut self,
+        mut move_selection: impl FnMut(&MultiBufferSnapshot, &mut Selection<usize>),
+    ) {
+        let mut changed = false;
+        let snapshot = self.buffer().clone();
+        let selections = self
+            .all::<usize>(self.cx)
+            .into_iter()
+            .map(|selection| {
+                let mut moved_selection = selection.clone();
+                move_selection(&snapshot, &mut moved_selection);
+                if selection != moved_selection {
+                    changed = true;
+                }
+                moved_selection
+            })
+            .collect();
+        drop(snapshot);
+
+        if changed {
+            self.select(selections)
+        }
+    }
+
+    pub fn move_heads_with(
+        &mut self,
+        mut update_head: impl FnMut(
+            &DisplaySnapshot,
+            DisplayPoint,
+            SelectionGoal,
+        ) -> (DisplayPoint, SelectionGoal),
+    ) {
+        self.move_with(|map, selection| {
+            let (new_head, new_goal) = update_head(map, selection.head(), selection.goal);
+            selection.set_head(new_head, new_goal);
+        });
+    }
+
+    pub fn move_cursors_with(
+        &mut self,
+        mut update_cursor_position: impl FnMut(
+            &DisplaySnapshot,
+            DisplayPoint,
+            SelectionGoal,
+        ) -> (DisplayPoint, SelectionGoal),
+    ) {
+        self.move_with(|map, selection| {
+            let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal);
+            selection.collapse_to(cursor, new_goal)
+        });
+    }
+
+    pub fn maybe_move_cursors_with(
+        &mut self,
+        mut update_cursor_position: impl FnMut(
+            &DisplaySnapshot,
+            DisplayPoint,
+            SelectionGoal,
+        ) -> Option<(DisplayPoint, SelectionGoal)>,
+    ) {
+        self.move_cursors_with(|map, point, goal| {
+            update_cursor_position(map, point, goal).unwrap_or((point, goal))
+        })
+    }
+
+    pub fn replace_cursors_with(
+        &mut self,
+        mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
+    ) {
+        let display_map = self.display_map();
+        let new_selections = find_replacement_cursors(&display_map)
+            .into_iter()
+            .map(|cursor| {
+                let cursor_point = cursor.to_point(&display_map);
+                Selection {
+                    id: post_inc(&mut self.collection.next_selection_id),
+                    start: cursor_point,
+                    end: cursor_point,
+                    reversed: false,
+                    goal: SelectionGoal::None,
+                }
+            })
+            .collect();
+        self.select(new_selections);
+    }
+
+    /// Compute new ranges for any selections that were located in excerpts that have
+    /// since been removed.
+    ///
+    /// Returns a `HashMap` indicating which selections whose former head position
+    /// was no longer present. The keys of the map are selection ids. The values are
+    /// the id of the new excerpt where the head of the selection has been moved.
+    pub fn refresh(&mut self) -> HashMap<usize, ExcerptId> {
+        let mut pending = self.collection.pending.take();
+        let mut selections_with_lost_position = HashMap::default();
+
+        let anchors_with_status = {
+            let buffer = self.buffer();
+            let disjoint_anchors = self
+                .disjoint
+                .iter()
+                .flat_map(|selection| [&selection.start, &selection.end]);
+            buffer.refresh_anchors(disjoint_anchors)
+        };
+        let adjusted_disjoint: Vec<_> = anchors_with_status
+            .chunks(2)
+            .map(|selection_anchors| {
+                let (anchor_ix, start, kept_start) = selection_anchors[0].clone();
+                let (_, end, kept_end) = selection_anchors[1].clone();
+                let selection = &self.disjoint[anchor_ix / 2];
+                let kept_head = if selection.reversed {
+                    kept_start
+                } else {
+                    kept_end
+                };
+                if !kept_head {
+                    selections_with_lost_position.insert(selection.id, selection.head().excerpt_id);
+                }
+
+                Selection {
+                    id: selection.id,
+                    start,
+                    end,
+                    reversed: selection.reversed,
+                    goal: selection.goal,
+                }
+            })
+            .collect();
+
+        if !adjusted_disjoint.is_empty() {
+            let resolved_selections =
+                resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect();
+            self.select::<usize>(resolved_selections);
+        }
+
+        if let Some(pending) = pending.as_mut() {
+            let buffer = self.buffer();
+            let anchors =
+                buffer.refresh_anchors([&pending.selection.start, &pending.selection.end]);
+            let (_, start, kept_start) = anchors[0].clone();
+            let (_, end, kept_end) = anchors[1].clone();
+            let kept_head = if pending.selection.reversed {
+                kept_start
+            } else {
+                kept_end
+            };
+            if !kept_head {
+                selections_with_lost_position
+                    .insert(pending.selection.id, pending.selection.head().excerpt_id);
+            }
+
+            pending.selection.start = start;
+            pending.selection.end = end;
+        }
+        self.collection.pending = pending;
+        self.selections_changed = true;
+
+        selections_with_lost_position
+    }
+}
+
+impl<'a> Deref for MutableSelectionsCollection<'a> {
+    type Target = SelectionsCollection;
+    fn deref(&self) -> &Self::Target {
+        self.collection
+    }
+}
+
+impl<'a> DerefMut for MutableSelectionsCollection<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.collection
+    }
+}
+
+// Panics if passed selections are not in order
+pub fn resolve_multiple<'a, D, I>(
+    selections: I,
+    snapshot: &MultiBufferSnapshot,
+) -> impl 'a + Iterator<Item = Selection<D>>
+where
+    D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
+    I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
+{
+    let (to_summarize, selections) = selections.into_iter().tee();
+    let mut summaries = snapshot
+        .summaries_for_anchors::<D, _>(
+            to_summarize
+                .flat_map(|s| [&s.start, &s.end])
+                .collect::<Vec<_>>(),
+        )
+        .into_iter();
+    selections.map(move |s| Selection {
+        id: s.id,
+        start: summaries.next().unwrap(),
+        end: summaries.next().unwrap(),
+        reversed: s.reversed,
+        goal: s.goal,
+    })
+}
+
+fn resolve<D: TextDimension + Ord + Sub<D, Output = D>>(
+    selection: &Selection<Anchor>,
+    buffer: &MultiBufferSnapshot,
+) -> Selection<D> {
+    selection.map(|p| p.summary::<D>(buffer))
+}

crates/editor2/src/test.rs ๐Ÿ”—

@@ -0,0 +1,83 @@
+pub mod editor_lsp_test_context;
+pub mod editor_test_context;
+
+use crate::{
+    display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+    DisplayPoint, Editor, EditorMode, MultiBuffer,
+};
+
+use gpui::{ModelHandle, ViewContext};
+
+use project::Project;
+use util::test::{marked_text_offsets, marked_text_ranges};
+
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}
+
+// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
+pub fn marked_display_snapshot(
+    text: &str,
+    cx: &mut gpui::AppContext,
+) -> (DisplaySnapshot, Vec<DisplayPoint>) {
+    let (unmarked_text, markers) = marked_text_offsets(text);
+
+    let family_id = cx
+        .font_cache()
+        .load_family(&["Helvetica"], &Default::default())
+        .unwrap();
+    let font_id = cx
+        .font_cache()
+        .select_font(family_id, &Default::default())
+        .unwrap();
+    let font_size = 14.0;
+
+    let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
+    let display_map =
+        cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
+    let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+    let markers = markers
+        .into_iter()
+        .map(|offset| offset.to_display_point(&snapshot))
+        .collect();
+
+    (snapshot, markers)
+}
+
+pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
+    let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
+    assert_eq!(editor.text(cx), unmarked_text);
+    editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
+}
+
+pub fn assert_text_with_selections(
+    editor: &mut Editor,
+    marked_text: &str,
+    cx: &mut ViewContext<Editor>,
+) {
+    let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
+    assert_eq!(editor.text(cx), unmarked_text);
+    assert_eq!(editor.selections.ranges(cx), text_ranges);
+}
+
+// RA thinks this is dead code even though it is used in a whole lot of tests
+#[allow(dead_code)]
+#[cfg(any(test, feature = "test-support"))]
+pub(crate) fn build_editor(
+    buffer: ModelHandle<MultiBuffer>,
+    cx: &mut ViewContext<Editor>,
+) -> Editor {
+    Editor::new(EditorMode::Full, buffer, None, None, cx)
+}
+
+pub(crate) fn build_editor_with_project(
+    project: ModelHandle<Project>,
+    buffer: ModelHandle<MultiBuffer>,
+    cx: &mut ViewContext<Editor>,
+) -> Editor {
+    Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
+}

crates/editor2/src/test/editor_lsp_test_context.rs ๐Ÿ”—

@@ -0,0 +1,297 @@
+use std::{
+    borrow::Cow,
+    ops::{Deref, DerefMut, Range},
+    sync::Arc,
+};
+
+use anyhow::Result;
+
+use crate::{Editor, ToPoint};
+use collections::HashSet;
+use futures::Future;
+use gpui::{json, ViewContext, ViewHandle};
+use indoc::indoc;
+use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
+use lsp::{notification, request};
+use multi_buffer::ToPointUtf16;
+use project::Project;
+use smol::stream::StreamExt;
+use workspace::{AppState, Workspace, WorkspaceHandle};
+
+use super::editor_test_context::EditorTestContext;
+
+pub struct EditorLspTestContext<'a> {
+    pub cx: EditorTestContext<'a>,
+    pub lsp: lsp::FakeLanguageServer,
+    pub workspace: ViewHandle<Workspace>,
+    pub buffer_lsp_url: lsp::Url,
+}
+
+impl<'a> EditorLspTestContext<'a> {
+    pub async fn new(
+        mut language: Language,
+        capabilities: lsp::ServerCapabilities,
+        cx: &'a mut gpui::TestAppContext,
+    ) -> EditorLspTestContext<'a> {
+        use json::json;
+
+        let app_state = cx.update(AppState::test);
+
+        cx.update(|cx| {
+            language::init(cx);
+            crate::init(cx);
+            workspace::init(app_state.clone(), cx);
+            Project::init_settings(cx);
+        });
+
+        let file_name = format!(
+            "file.{}",
+            language
+                .path_suffixes()
+                .first()
+                .expect("language must have a path suffix for EditorLspTestContext")
+        );
+
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities,
+                ..Default::default()
+            }))
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
+            .await;
+
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
+        project
+            .update(cx, |project, cx| {
+                project.find_or_create_local_worktree("/root", true, cx)
+            })
+            .await
+            .unwrap();
+        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
+            .await;
+
+        let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
+        let item = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path(file, None, true, cx)
+            })
+            .await
+            .expect("Could not open test file");
+
+        let editor = cx.update(|cx| {
+            item.act_as::<Editor>(cx)
+                .expect("Opened test file wasn't an editor")
+        });
+        editor.update(cx, |_, cx| cx.focus_self());
+
+        let lsp = fake_servers.next().await.unwrap();
+
+        Self {
+            cx: EditorTestContext {
+                cx,
+                window: window.into(),
+                editor,
+            },
+            lsp,
+            workspace,
+            buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
+        }
+    }
+
+    pub async fn new_rust(
+        capabilities: lsp::ServerCapabilities,
+        cx: &'a mut gpui::TestAppContext,
+    ) -> EditorLspTestContext<'a> {
+        let language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_queries(LanguageQueries {
+            indents: Some(Cow::from(indoc! {r#"
+                [
+                    ((where_clause) _ @end)
+                    (field_expression)
+                    (call_expression)
+                    (assignment_expression)
+                    (let_declaration)
+                    (let_chain)
+                    (await_expression)
+                ] @indent
+
+                (_ "[" "]" @end) @indent
+                (_ "<" ">" @end) @indent
+                (_ "{" "}" @end) @indent
+                (_ "(" ")" @end) @indent"#})),
+            brackets: Some(Cow::from(indoc! {r#"
+                ("(" @open ")" @close)
+                ("[" @open "]" @close)
+                ("{" @open "}" @close)
+                ("<" @open ">" @close)
+                ("\"" @open "\"" @close)
+                (closure_parameters "|" @open "|" @close)"#})),
+            ..Default::default()
+        })
+        .expect("Could not parse queries");
+
+        Self::new(language, capabilities, cx).await
+    }
+
+    pub async fn new_typescript(
+        capabilities: lsp::ServerCapabilities,
+        cx: &'a mut gpui::TestAppContext,
+    ) -> EditorLspTestContext<'a> {
+        let mut word_characters: HashSet<char> = Default::default();
+        word_characters.insert('$');
+        word_characters.insert('#');
+        let language = Language::new(
+            LanguageConfig {
+                name: "Typescript".into(),
+                path_suffixes: vec!["ts".to_string()],
+                brackets: language::BracketPairConfig {
+                    pairs: vec![language::BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        newline: true,
+                    }],
+                    disabled_scopes_by_bracket_ix: Default::default(),
+                },
+                word_characters,
+                ..Default::default()
+            },
+            Some(tree_sitter_typescript::language_typescript()),
+        )
+        .with_queries(LanguageQueries {
+            brackets: Some(Cow::from(indoc! {r#"
+                ("(" @open ")" @close)
+                ("[" @open "]" @close)
+                ("{" @open "}" @close)
+                ("<" @open ">" @close)
+                ("\"" @open "\"" @close)"#})),
+            indents: Some(Cow::from(indoc! {r#"
+                [
+                    (call_expression)
+                    (assignment_expression)
+                    (member_expression)
+                    (lexical_declaration)
+                    (variable_declaration)
+                    (assignment_expression)
+                    (if_statement)
+                    (for_statement)
+                ] @indent
+
+                (_ "[" "]" @end) @indent
+                (_ "<" ">" @end) @indent
+                (_ "{" "}" @end) @indent
+                (_ "(" ")" @end) @indent
+                "#})),
+            ..Default::default()
+        })
+        .expect("Could not parse queries");
+
+        Self::new(language, capabilities, cx).await
+    }
+
+    // Constructs lsp range using a marked string with '[', ']' range delimiters
+    pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
+        let ranges = self.ranges(marked_text);
+        self.to_lsp_range(ranges[0].clone())
+    }
+
+    pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
+        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+        let start_point = range.start.to_point(&snapshot.buffer_snapshot);
+        let end_point = range.end.to_point(&snapshot.buffer_snapshot);
+
+        self.editor(|editor, cx| {
+            let buffer = editor.buffer().read(cx);
+            let start = point_to_lsp(
+                buffer
+                    .point_to_buffer_offset(start_point, cx)
+                    .unwrap()
+                    .1
+                    .to_point_utf16(&buffer.read(cx)),
+            );
+            let end = point_to_lsp(
+                buffer
+                    .point_to_buffer_offset(end_point, cx)
+                    .unwrap()
+                    .1
+                    .to_point_utf16(&buffer.read(cx)),
+            );
+
+            lsp::Range { start, end }
+        })
+    }
+
+    pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
+        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+        let point = offset.to_point(&snapshot.buffer_snapshot);
+
+        self.editor(|editor, cx| {
+            let buffer = editor.buffer().read(cx);
+            point_to_lsp(
+                buffer
+                    .point_to_buffer_offset(point, cx)
+                    .unwrap()
+                    .1
+                    .to_point_utf16(&buffer.read(cx)),
+            )
+        })
+    }
+
+    pub fn update_workspace<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
+    {
+        self.workspace.update(self.cx.cx, update)
+    }
+
+    pub fn handle_request<T, F, Fut>(
+        &self,
+        mut handler: F,
+    ) -> futures::channel::mpsc::UnboundedReceiver<()>
+    where
+        T: 'static + request::Request,
+        T::Params: 'static + Send,
+        F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
+        Fut: 'static + Send + Future<Output = Result<T::Result>>,
+    {
+        let url = self.buffer_lsp_url.clone();
+        self.lsp.handle_request::<T, _, _>(move |params, cx| {
+            let url = url.clone();
+            handler(url, params, cx)
+        })
+    }
+
+    pub fn notify<T: notification::Notification>(&self, params: T::Params) {
+        self.lsp.notify::<T>(params);
+    }
+}
+
+impl<'a> Deref for EditorLspTestContext<'a> {
+    type Target = EditorTestContext<'a>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.cx
+    }
+}
+
+impl<'a> DerefMut for EditorLspTestContext<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
+    }
+}

crates/editor2/src/test/editor_test_context.rs ๐Ÿ”—

@@ -0,0 +1,332 @@
+use crate::{
+    display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
+};
+use futures::Future;
+use gpui::{
+    executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle,
+    ModelContext, ViewContext, ViewHandle,
+};
+use indoc::indoc;
+use language::{Buffer, BufferSnapshot};
+use project::{FakeFs, Project};
+use std::{
+    any::TypeId,
+    ops::{Deref, DerefMut, Range},
+};
+use util::{
+    assert_set_eq,
+    test::{generate_marked_text, marked_text_ranges},
+};
+
+use super::build_editor_with_project;
+
+pub struct EditorTestContext<'a> {
+    pub cx: &'a mut gpui::TestAppContext,
+    pub window: AnyWindowHandle,
+    pub editor: ViewHandle<Editor>,
+}
+
+impl<'a> EditorTestContext<'a> {
+    pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
+        let fs = FakeFs::new(cx.background());
+        // fs.insert_file("/file", "".to_owned()).await;
+        fs.insert_tree(
+            "/root",
+            gpui::serde_json::json!({
+                "file": "",
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/root".as_ref()], cx).await;
+        let buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/root/file", cx)
+            })
+            .await
+            .unwrap();
+        let window = cx.add_window(|cx| {
+            cx.focus_self();
+            build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx)
+        });
+        let editor = window.root(cx);
+        Self {
+            cx,
+            window: window.into(),
+            editor,
+        }
+    }
+
+    pub fn condition(
+        &self,
+        predicate: impl FnMut(&Editor, &AppContext) -> bool,
+    ) -> impl Future<Output = ()> {
+        self.editor.condition(self.cx, predicate)
+    }
+
+    pub fn editor<F, T>(&self, read: F) -> T
+    where
+        F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
+    {
+        self.editor.read_with(self.cx, read)
+    }
+
+    pub fn update_editor<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
+    {
+        self.editor.update(self.cx, update)
+    }
+
+    pub fn multibuffer<F, T>(&self, read: F) -> T
+    where
+        F: FnOnce(&MultiBuffer, &AppContext) -> T,
+    {
+        self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
+    }
+
+    pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
+    {
+        self.update_editor(|editor, cx| editor.buffer().update(cx, update))
+    }
+
+    pub fn buffer_text(&self) -> String {
+        self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
+    }
+
+    pub fn buffer<F, T>(&self, read: F) -> T
+    where
+        F: FnOnce(&Buffer, &AppContext) -> T,
+    {
+        self.multibuffer(|multibuffer, cx| {
+            let buffer = multibuffer.as_singleton().unwrap().read(cx);
+            read(buffer, cx)
+        })
+    }
+
+    pub fn update_buffer<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
+    {
+        self.update_multibuffer(|multibuffer, cx| {
+            let buffer = multibuffer.as_singleton().unwrap();
+            buffer.update(cx, update)
+        })
+    }
+
+    pub fn buffer_snapshot(&self) -> BufferSnapshot {
+        self.buffer(|buffer, _| buffer.snapshot())
+    }
+
+    pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
+        let keystroke_under_test_handle =
+            self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
+        let keystroke = Keystroke::parse(keystroke_text).unwrap();
+
+        self.cx.dispatch_keystroke(self.window, keystroke, false);
+
+        keystroke_under_test_handle
+    }
+
+    pub fn simulate_keystrokes<const COUNT: usize>(
+        &mut self,
+        keystroke_texts: [&str; COUNT],
+    ) -> ContextHandle {
+        let keystrokes_under_test_handle =
+            self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
+        for keystroke_text in keystroke_texts.into_iter() {
+            self.simulate_keystroke(keystroke_text);
+        }
+        // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
+        // before returning.
+        // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
+        // quickly races with async actions.
+        if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
+            executor.run_until_parked();
+        } else {
+            unreachable!();
+        }
+
+        keystrokes_under_test_handle
+    }
+
+    pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
+        let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
+        assert_eq!(self.buffer_text(), unmarked_text);
+        ranges
+    }
+
+    pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
+        let ranges = self.ranges(marked_text);
+        let snapshot = self
+            .editor
+            .update(self.cx, |editor, cx| editor.snapshot(cx));
+        ranges[0].start.to_display_point(&snapshot)
+    }
+
+    // Returns anchors for the current buffer using `ยซ` and `ยป`
+    pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
+        let ranges = self.ranges(marked_text);
+        let snapshot = self.buffer_snapshot();
+        snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
+    }
+
+    pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
+        let diff_base = diff_base.map(String::from);
+        self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
+    }
+
+    /// Change the editor's text and selections using a string containing
+    /// embedded range markers that represent the ranges and directions of
+    /// each selection.
+    ///
+    /// Returns a context handle so that assertion failures can print what
+    /// editor state was needed to cause the failure.
+    ///
+    /// See the `util::test::marked_text_ranges` function for more information.
+    pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
+        let state_context = self.add_assertion_context(format!(
+            "Initial Editor State: \"{}\"",
+            marked_text.escape_debug().to_string()
+        ));
+        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
+        self.editor.update(self.cx, |editor, cx| {
+            editor.set_text(unmarked_text, cx);
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select_ranges(selection_ranges)
+            })
+        });
+        state_context
+    }
+
+    /// Only change the editor's selections
+    pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
+        let state_context = self.add_assertion_context(format!(
+            "Initial Editor State: \"{}\"",
+            marked_text.escape_debug().to_string()
+        ));
+        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
+        self.editor.update(self.cx, |editor, cx| {
+            assert_eq!(editor.text(cx), unmarked_text);
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select_ranges(selection_ranges)
+            })
+        });
+        state_context
+    }
+
+    /// Make an assertion about the editor's text and the ranges and directions
+    /// of its selections using a string containing embedded range markers.
+    ///
+    /// See the `util::test::marked_text_ranges` function for more information.
+    #[track_caller]
+    pub fn assert_editor_state(&mut self, marked_text: &str) {
+        let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
+        let buffer_text = self.buffer_text();
+
+        if buffer_text != unmarked_text {
+            panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
+        }
+
+        self.assert_selections(expected_selections, marked_text.to_string())
+    }
+
+    pub fn editor_state(&mut self) -> String {
+        generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
+    }
+
+    #[track_caller]
+    pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
+        let expected_ranges = self.ranges(marked_text);
+        let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            editor
+                .background_highlights
+                .get(&TypeId::of::<Tag>())
+                .map(|h| h.1.clone())
+                .unwrap_or_default()
+                .into_iter()
+                .map(|range| range.to_offset(&snapshot.buffer_snapshot))
+                .collect()
+        });
+        assert_set_eq!(actual_ranges, expected_ranges);
+    }
+
+    #[track_caller]
+    pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
+        let expected_ranges = self.ranges(marked_text);
+        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+        let actual_ranges: Vec<Range<usize>> = snapshot
+            .text_highlight_ranges::<Tag>()
+            .map(|ranges| ranges.as_ref().clone().1)
+            .unwrap_or_default()
+            .into_iter()
+            .map(|range| range.to_offset(&snapshot.buffer_snapshot))
+            .collect();
+        assert_set_eq!(actual_ranges, expected_ranges);
+    }
+
+    #[track_caller]
+    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
+        let expected_marked_text =
+            generate_marked_text(&self.buffer_text(), &expected_selections, true);
+        self.assert_selections(expected_selections, expected_marked_text)
+    }
+
+    fn editor_selections(&self) -> Vec<Range<usize>> {
+        self.editor
+            .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
+            .into_iter()
+            .map(|s| {
+                if s.reversed {
+                    s.end..s.start
+                } else {
+                    s.start..s.end
+                }
+            })
+            .collect::<Vec<_>>()
+    }
+
+    #[track_caller]
+    fn assert_selections(
+        &mut self,
+        expected_selections: Vec<Range<usize>>,
+        expected_marked_text: String,
+    ) {
+        let actual_selections = self.editor_selections();
+        let actual_marked_text =
+            generate_marked_text(&self.buffer_text(), &actual_selections, true);
+        if expected_selections != actual_selections {
+            panic!(
+                indoc! {"
+
+                    {}Editor has unexpected selections.
+
+                    Expected selections:
+                    {}
+
+                    Actual selections:
+                    {}
+                "},
+                self.assertion_context(),
+                expected_marked_text,
+                actual_marked_text,
+            );
+        }
+    }
+}
+
+impl<'a> Deref for EditorTestContext<'a> {
+    type Target = gpui::TestAppContext;
+
+    fn deref(&self) -> &Self::Target {
+        self.cx
+    }
+}
+
+impl<'a> DerefMut for EditorTestContext<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
+    }
+}