Editor2 tests (#3486)

Piotr Osiewicz created

Release Notes:

- N/A

Change summary

Cargo.lock                                       |    2 
crates/copilot2/Cargo.toml                       |    2 
crates/copilot2/src/copilot2.rs                  |  454 ++--
crates/editor2/src/display_map.rs                | 1768 ++++++++---------
crates/editor2/src/display_map/block_map.rs      | 1338 ++++++------
crates/editor2/src/display_map/wrap_map.rs       |  750 +++---
crates/editor2/src/editor.rs                     |    7 
crates/editor2/src/editor_tests.rs               | 1588 ++++++++-------
crates/editor2/src/element.rs                    |  929 ++++----
crates/editor2/src/git.rs                        |  364 +-
crates/editor2/src/highlight_matching_bracket.rs |  200 +-
crates/editor2/src/inlay_hint_cache.rs           |  173 
crates/editor2/src/link_go_to_definition.rs      | 1337 ++++++------
crates/editor2/src/mouse_context_menu.rs         |   71 
crates/editor2/src/movement.rs                   |  952 ++++----
crates/gpui2/src/app.rs                          |   37 
crates/gpui2/src/app/model_context.rs            |   24 
crates/gpui2/src/app/test_context.rs             |   56 
crates/gpui2/src/executor.rs                     |   42 
crates/gpui2/src/gpui2.rs                        |    2 
crates/gpui2/src/platform.rs                     |    6 
crates/gpui2/src/platform/test/dispatcher.rs     |   17 
crates/gpui2/src/platform/test/window.rs         |    4 
crates/gpui2/src/scene.rs                        |    4 
crates/gpui2/src/style.rs                        |    3 
crates/gpui2/src/subscription.rs                 |   50 
crates/gpui2/src/test.rs                         |   31 
crates/gpui2/src/window.rs                       |   78 
28 files changed, 5,244 insertions(+), 5,045 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2112,7 +2112,7 @@ dependencies = [
  "lsp2",
  "node_runtime",
  "parking_lot 0.11.2",
- "rpc",
+ "rpc2",
  "serde",
  "serde_derive",
  "settings2",

crates/copilot2/Cargo.toml 🔗

@@ -45,6 +45,6 @@ fs = { path = "../fs", features = ["test-support"] }
 gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
 language = { package = "language2", path = "../language2", features = ["test-support"] }
 lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
+rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
 settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }

crates/copilot2/src/copilot2.rs 🔗

@@ -1002,229 +1002,231 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use gpui::{executor::Deterministic, TestAppContext};
-
-//     #[gpui::test(iterations = 10)]
-//     async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-//         deterministic.forbid_parking();
-//         let (copilot, mut lsp) = Copilot::fake(cx);
-
-//         let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello"));
-//         let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
-//         copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-//                 .await,
-//             lsp::DidOpenTextDocumentParams {
-//                 text_document: lsp::TextDocumentItem::new(
-//                     buffer_1_uri.clone(),
-//                     "plaintext".into(),
-//                     0,
-//                     "Hello".into()
-//                 ),
-//             }
-//         );
-
-//         let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye"));
-//         let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
-//         copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-//                 .await,
-//             lsp::DidOpenTextDocumentParams {
-//                 text_document: lsp::TextDocumentItem::new(
-//                     buffer_2_uri.clone(),
-//                     "plaintext".into(),
-//                     0,
-//                     "Goodbye".into()
-//                 ),
-//             }
-//         );
-
-//         buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
-//                 .await,
-//             lsp::DidChangeTextDocumentParams {
-//                 text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
-//                 content_changes: vec![lsp::TextDocumentContentChangeEvent {
-//                     range: Some(lsp::Range::new(
-//                         lsp::Position::new(0, 5),
-//                         lsp::Position::new(0, 5)
-//                     )),
-//                     range_length: None,
-//                     text: " world".into(),
-//                 }],
-//             }
-//         );
-
-//         // Ensure updates to the file are reflected in the LSP.
-//         buffer_1
-//             .update(cx, |buffer, cx| {
-//                 buffer.file_updated(
-//                     Arc::new(File {
-//                         abs_path: "/root/child/buffer-1".into(),
-//                         path: Path::new("child/buffer-1").into(),
-//                     }),
-//                     cx,
-//                 )
-//             })
-//             .await;
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-//                 .await,
-//             lsp::DidCloseTextDocumentParams {
-//                 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
-//             }
-//         );
-//         let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-//                 .await,
-//             lsp::DidOpenTextDocumentParams {
-//                 text_document: lsp::TextDocumentItem::new(
-//                     buffer_1_uri.clone(),
-//                     "plaintext".into(),
-//                     1,
-//                     "Hello world".into()
-//                 ),
-//             }
-//         );
-
-//         // Ensure all previously-registered buffers are closed when signing out.
-//         lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
-//             Ok(request::SignOutResult {})
-//         });
-//         copilot
-//             .update(cx, |copilot, cx| copilot.sign_out(cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-//                 .await,
-//             lsp::DidCloseTextDocumentParams {
-//                 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
-//             }
-//         );
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-//                 .await,
-//             lsp::DidCloseTextDocumentParams {
-//                 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
-//             }
-//         );
-
-//         // Ensure all previously-registered buffers are re-opened when signing in.
-//         lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
-//             Ok(request::SignInInitiateResult::AlreadySignedIn {
-//                 user: "user-1".into(),
-//             })
-//         });
-//         copilot
-//             .update(cx, |copilot, cx| copilot.sign_in(cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-//                 .await,
-//             lsp::DidOpenTextDocumentParams {
-//                 text_document: lsp::TextDocumentItem::new(
-//                     buffer_2_uri.clone(),
-//                     "plaintext".into(),
-//                     0,
-//                     "Goodbye".into()
-//                 ),
-//             }
-//         );
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-//                 .await,
-//             lsp::DidOpenTextDocumentParams {
-//                 text_document: lsp::TextDocumentItem::new(
-//                     buffer_1_uri.clone(),
-//                     "plaintext".into(),
-//                     0,
-//                     "Hello world".into()
-//                 ),
-//             }
-//         );
-
-//         // Dropping a buffer causes it to be closed on the LSP side as well.
-//         cx.update(|_| drop(buffer_2));
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-//                 .await,
-//             lsp::DidCloseTextDocumentParams {
-//                 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
-//             }
-//         );
-//     }
-
-//     struct File {
-//         abs_path: PathBuf,
-//         path: Arc<Path>,
-//     }
-
-//     impl language2::File for File {
-//         fn as_local(&self) -> Option<&dyn language2::LocalFile> {
-//             Some(self)
-//         }
-
-//         fn mtime(&self) -> std::time::SystemTime {
-//             unimplemented!()
-//         }
-
-//         fn path(&self) -> &Arc<Path> {
-//             &self.path
-//         }
-
-//         fn full_path(&self, _: &AppContext) -> PathBuf {
-//             unimplemented!()
-//         }
-
-//         fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
-//             unimplemented!()
-//         }
-
-//         fn is_deleted(&self) -> bool {
-//             unimplemented!()
-//         }
-
-//         fn as_any(&self) -> &dyn std::any::Any {
-//             unimplemented!()
-//         }
-
-//         fn to_proto(&self) -> rpc::proto::File {
-//             unimplemented!()
-//         }
-
-//         fn worktree_id(&self) -> usize {
-//             0
-//         }
-//     }
-
-//     impl language::LocalFile for File {
-//         fn abs_path(&self, _: &AppContext) -> PathBuf {
-//             self.abs_path.clone()
-//         }
-
-//         fn load(&self, _: &AppContext) -> Task<Result<String>> {
-//             unimplemented!()
-//         }
-
-//         fn buffer_reloaded(
-//             &self,
-//             _: u64,
-//             _: &clock::Global,
-//             _: language::RopeFingerprint,
-//             _: language::LineEnding,
-//             _: std::time::SystemTime,
-//             _: &mut AppContext,
-//         ) {
-//             unimplemented!()
-//         }
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::TestAppContext;
+
+    #[gpui::test(iterations = 10)]
+    async fn test_buffer_management(cx: &mut TestAppContext) {
+        let (copilot, mut lsp) = Copilot::fake(cx);
+
+        let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Hello"));
+        let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
+            .parse()
+            .unwrap();
+        copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Hello".into()
+                ),
+            }
+        );
+
+        let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Goodbye"));
+        let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
+            .parse()
+            .unwrap();
+        copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_2_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Goodbye".into()
+                ),
+            }
+        );
+
+        buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
+                .await,
+            lsp::DidChangeTextDocumentParams {
+                text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
+                content_changes: vec![lsp::TextDocumentContentChangeEvent {
+                    range: Some(lsp::Range::new(
+                        lsp::Position::new(0, 5),
+                        lsp::Position::new(0, 5)
+                    )),
+                    range_length: None,
+                    text: " world".into(),
+                }],
+            }
+        );
+
+        // Ensure updates to the file are reflected in the LSP.
+        buffer_1.update(cx, |buffer, cx| {
+            buffer.file_updated(
+                Arc::new(File {
+                    abs_path: "/root/child/buffer-1".into(),
+                    path: Path::new("child/buffer-1").into(),
+                }),
+                cx,
+            )
+        });
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
+            }
+        );
+        let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    1,
+                    "Hello world".into()
+                ),
+            }
+        );
+
+        // Ensure all previously-registered buffers are closed when signing out.
+        lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
+            Ok(request::SignOutResult {})
+        });
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_out(cx))
+            .await
+            .unwrap();
+        // todo!() po: these notifications now happen in reverse order?
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
+            }
+        );
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
+            }
+        );
+
+        // Ensure all previously-registered buffers are re-opened when signing in.
+        lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
+            Ok(request::SignInInitiateResult::AlreadySignedIn {
+                user: "user-1".into(),
+            })
+        });
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_in(cx))
+            .await
+            .unwrap();
+
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Hello world".into()
+                ),
+            }
+        );
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_2_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Goodbye".into()
+                ),
+            }
+        );
+        // Dropping a buffer causes it to be closed on the LSP side as well.
+        cx.update(|_| drop(buffer_2));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
+            }
+        );
+    }
+
+    struct File {
+        abs_path: PathBuf,
+        path: Arc<Path>,
+    }
+
+    impl language::File for File {
+        fn as_local(&self) -> Option<&dyn language::LocalFile> {
+            Some(self)
+        }
+
+        fn mtime(&self) -> std::time::SystemTime {
+            unimplemented!()
+        }
+
+        fn path(&self) -> &Arc<Path> {
+            &self.path
+        }
+
+        fn full_path(&self, _: &AppContext) -> PathBuf {
+            unimplemented!()
+        }
+
+        fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
+            unimplemented!()
+        }
+
+        fn is_deleted(&self) -> bool {
+            unimplemented!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            unimplemented!()
+        }
+
+        fn to_proto(&self) -> rpc::proto::File {
+            unimplemented!()
+        }
+
+        fn worktree_id(&self) -> usize {
+            0
+        }
+    }
+
+    impl language::LocalFile for File {
+        fn abs_path(&self, _: &AppContext) -> PathBuf {
+            self.abs_path.clone()
+        }
+
+        fn load(&self, _: &AppContext) -> Task<Result<String>> {
+            unimplemented!()
+        }
+
+        fn buffer_reloaded(
+            &self,
+            _: u64,
+            _: &clock::Global,
+            _: language::RopeFingerprint,
+            _: language::LineEnding,
+            _: std::time::SystemTime,
+            _: &mut AppContext,
+        ) {
+            unimplemented!()
+        }
+    }
+}

crates/editor2/src/display_map.rs 🔗

@@ -990,905 +990,869 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat
     })
 }
 
-// #[cfg(test)]
-// pub mod tests {
-//     use super::*;
-//     use crate::{
-//         movement,
-//         test::{editor_test_context::EditorTestContext, marked_display_snapshot},
-//     };
-//     use gpui::{AppContext, Hsla};
-//     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_with_size(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(), Hsla::red().into()),
-//             ("fn.name".to_string(), Hsla::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(Hsla::blue())),
-//                 ("() {}\n\nmod module ".to_string(), None),
-//                 ("{\n    fn ".to_string(), Some(Hsla::red())),
-//                 ("inner".to_string(), Some(Hsla::blue())),
-//                 ("() {}\n}".to_string(), Some(Hsla::red())),
-//             ]
-//         );
-//         assert_eq!(
-//             cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
-//             vec![
-//                 ("    fn ".to_string(), Some(Hsla::red())),
-//                 ("inner".to_string(), Some(Hsla::blue())),
-//                 ("() {}\n}".to_string(), Some(Hsla::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(Hsla::blue())),
-//                 ("⋯".to_string(), None),
-//                 ("  fn ".to_string(), Some(Hsla::red())),
-//                 ("inner".to_string(), Some(Hsla::blue())),
-//                 ("() {}\n}".to_string(), Some(Hsla::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(), Hsla::red().into()),
-//             ("fn.name".to_string(), Hsla::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(Hsla::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(Hsla::blue())),
-//                 ("⋯\n".to_string(), None),
-//                 ("  \nfn ".to_string(), Some(Hsla::red())),
-//                 ("i\n".to_string(), Some(Hsla::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(), Hsla::red().into()),
-//             ("string".to_string(), Hsla::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(Hsla::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(Hsla::blue())),
-//                 (":".to_string(), Some(Hsla::red()), None),
-//                 (" B = ".to_string(), None, None),
-//                 ("\"c ".to_string(), Some(Hsla::green()), None),
-//                 ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())),
-//                 ("\"".to_string(), Some(Hsla::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: &Model<DisplayMap>,
-//         theme: &'a SyntaxTheme,
-//         cx: &mut AppContext,
-//     ) -> Vec<(String, Option<Hsla>)> {
-//         chunks(rows, map, theme, cx)
-//             .into_iter()
-//             .map(|(text, color, _)| (text, color))
-//             .collect()
-//     }
-
-//     fn chunks<'a>(
-//         rows: Range<u32>,
-//         map: &Model<DisplayMap>,
-//         theme: &'a SyntaxTheme,
-//         cx: &mut AppContext,
-//     ) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
-//         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-//         let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = 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);
-//         });
-//     }
-// }
+#[cfg(test)]
+pub mod tests {
+    use super::*;
+    use crate::{
+        movement,
+        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+    };
+    use gpui::{div, font, observe, px, AppContext, Context, Element, Hsla};
+    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::{LoadThemes, 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.background_executor.set_block_on_ticks(0..=50);
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let test_platform = &cx.test_platform;
+        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 font_size = px(14.0);
+        let max_wrap_width = 300.0;
+        let mut wrap_width = if rng.gen_bool(0.1) {
+            None
+        } else {
+            Some(px(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.build_model(|cx| {
+            DisplayMap::new(
+                buffer.clone(),
+                font("Helvetica"),
+                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(px(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(|_| div().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.background_executor
+            .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.update(cx, |editor, cx| editor.text_layout_details(cx));
+
+            let font_size = px(12.0);
+            let wrap_width = Some(px(64.));
+
+            let text = "one two three four five\nsix seven eight";
+            let buffer = MultiBuffer::build_simple(text, cx);
+            let map = cx.build_model(|cx| {
+                DisplayMap::new(
+                    buffer.clone(),
+                    font("Helvetica"),
+                    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_display_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.0)
+                )
+            );
+            assert_eq!(
+                movement::down(
+                    &snapshot,
+                    DisplayPoint::new(0, 7),
+                    SelectionGoal::HorizontalPosition(x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::HorizontalPosition(x.0)
+                )
+            );
+            assert_eq!(
+                movement::down(
+                    &snapshot,
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::HorizontalPosition(x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 4),
+                    SelectionGoal::HorizontalPosition(x.0)
+                )
+            );
+
+            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("Helvetica"), px(font_size.0 + 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 font_size = px(14.0);
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(buffer.clone(), font("Helvetica"), 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_test(vec![
+            ("mod.body", Hsla::red().into()),
+            ("fn.name", Hsla::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.build_model(|cx| {
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+        });
+        cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
+        let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+
+        let font_size = px(14.0);
+
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(buffer, font("Helvetica"), 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(Hsla::blue())),
+                ("() {}\n\nmod module ".to_string(), None),
+                ("{\n    fn ".to_string(), Some(Hsla::red())),
+                ("inner".to_string(), Some(Hsla::blue())),
+                ("() {}\n}".to_string(), Some(Hsla::red())),
+            ]
+        );
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
+            vec![
+                ("    fn ".to_string(), Some(Hsla::red())),
+                ("inner".to_string(), Some(Hsla::blue())),
+                ("() {}\n}".to_string(), Some(Hsla::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(Hsla::blue())),
+                ("⋯".to_string(), None),
+                ("  fn ".to_string(), Some(Hsla::red())),
+                ("inner".to_string(), Some(Hsla::blue())),
+                ("() {}\n}".to_string(), Some(Hsla::red())),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
+        use unindent::Unindent as _;
+
+        cx.background_executor
+            .set_block_on_ticks(usize::MAX..=usize::MAX);
+
+        let text = r#"
+            fn outer() {}
+
+            mod module {
+                fn inner() {}
+            }"#
+        .unindent();
+
+        let theme = SyntaxTheme::new_test(vec![
+            ("mod.body", Hsla::red().into()),
+            ("fn.name", Hsla::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.build_model(|cx| {
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+        });
+        cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
+        let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+
+        let font_size = px(16.0);
+
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(buffer, font("Courier"), font_size, Some(px(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(Hsla::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(Hsla::blue())),
+                ("⋯\n".to_string(), None),
+                ("  \nfn ".to_string(), Some(Hsla::red())),
+                ("i\n".to_string(), Some(Hsla::blue()))
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| init_test(cx, |_| {}));
+
+        let theme = SyntaxTheme::new_test(vec![
+            ("operator", Hsla::red().into()),
+            ("string", Hsla::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.build_model(|cx| {
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+        });
+        cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
+
+        let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+        let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+
+        let font_size = px(16.0);
+        let map = cx
+            .build_model(|cx| DisplayMap::new(buffer, font("Courier"), font_size, None, 1, 1, cx));
+
+        enum MyType {}
+
+        let style = HighlightStyle {
+            color: Some(Hsla::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(Hsla::blue())),
+                (":".to_string(), Some(Hsla::red()), None),
+                (" B = ".to_string(), None, None),
+                ("\"c ".to_string(), Some(Hsla::green()), None),
+                ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())),
+                ("\"".to_string(), Some(Hsla::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_size = px(14.0);
+
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(buffer.clone(), font("Helvetica"), 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_size = px(14.0);
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(buffer.clone(), font("Helvetica"), 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: &Model<DisplayMap>,
+        theme: &'a SyntaxTheme,
+        cx: &mut AppContext,
+    ) -> Vec<(String, Option<Hsla>)> {
+        chunks(rows, map, theme, cx)
+            .into_iter()
+            .map(|(text, color, _)| (text, color))
+            .collect()
+    }
+
+    fn chunks<'a>(
+        rows: Range<u32>,
+        map: &Model<DisplayMap>,
+        theme: &'a SyntaxTheme,
+        cx: &mut AppContext,
+    ) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+        let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = 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)) {
+        let settings = SettingsStore::test(cx);
+        cx.set_global(settings);
+        language::init(cx);
+        crate::init(cx);
+        Project::init_settings(cx);
+        theme::init(LoadThemes::JustBase, cx);
+        cx.update_global::<SettingsStore, _>(|store, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, f);
+        });
+    }
+}

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

@@ -988,680 +988,664 @@ fn offset_for_row(s: &str, target: u32) -> (u32, 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::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)
-//         }
-//     }
-// }
+#[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::{div, font, px, Element, Platform as _};
+    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::TestAppContext) {
+        cx.update(|cx| init_test(cx));
+
+        let text = "aaa\nbbb\nccc\nddd";
+
+        let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
+        let buffer_snapshot = cx.update(|cx| 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) =
+            cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(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(|_| div().into_any()),
+            },
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(1, 2)),
+                height: 2,
+                disposition: BlockDisposition::Above,
+                render: Arc::new(|_| div().into_any()),
+            },
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(3, 3)),
+                height: 3,
+                disposition: BlockDisposition::Below,
+                render: Arc::new(|_| div().into_any()),
+            },
+        ]);
+
+        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::TestAppContext) {
+        cx.update(|cx| init_test(cx));
+
+        let font_id = cx
+            .test_platform
+            .text_system()
+            .font_id(&font("Helvetica"))
+            .unwrap();
+
+        let text = "one two three\nfour five six\nseven eight";
+
+        let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
+        let buffer_snapshot = cx.update(|cx| 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) = cx.update(|cx| {
+            WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(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(|_| div().into_any()),
+                height: 1,
+            },
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(1, 1)),
+                disposition: BlockDisposition::Below,
+                render: Arc::new(|_| div().into_any()),
+                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::TestAppContext, mut rng: StdRng) {
+        cx.update(|cx| 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(px(rng.gen_range(0.0..=100.0)))
+        };
+        let tab_size = 1.try_into().unwrap();
+        let font_size = px(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);
+            cx.update(|cx| MultiBuffer::build_simple(&text, cx))
+        } else {
+            cx.update(|cx| MultiBuffer::build_random(&mut rng, cx))
+        };
+
+        let mut buffer_snapshot = cx.update(|cx| 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) = cx
+            .update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), 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(px(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 = cx.update(|cx| buffer.read(cx).read(cx).clone());
+                            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(|_| div().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) {
+        let settings = SettingsStore::test(cx);
+        cx.set_global(settings);
+        theme::init(theme::LoadThemes::JustBase, 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/wrap_map.rs 🔗

@@ -741,49 +741,48 @@ impl WrapSnapshot {
     }
 
     fn check_invariants(&self) {
-        // todo!()
-        // #[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
-        //         );
-        //     }
-        // }
+        #[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
+                );
+            }
+        }
     }
 }
 
@@ -1026,337 +1025,334 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
     }
 }
 
-// #[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
-//                 );
-//             }
-//         }
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
+        MultiBuffer,
+    };
+    use gpui::{font, px, test::observe, Platform};
+    use rand::prelude::*;
+    use settings::SettingsStore;
+    use smol::stream::StreamExt;
+    use std::{cmp, env, num::NonZeroU32};
+    use text::Rope;
+    use theme::LoadThemes;
+
+    #[gpui::test(iterations = 100)]
+    async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+        // todo!() this test is flaky
+        init_test(cx);
+
+        cx.background_executor.set_block_on_ticks(0..=50);
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let text_system = cx.read(|cx| cx.text_system().clone());
+        let mut wrap_width = if rng.gen_bool(0.1) {
+            None
+        } else {
+            Some(px(rng.gen_range(0.0..=1000.0)))
+        };
+        let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
+        let font = font("Helvetica");
+        let font_id = text_system.font_id(&font).unwrap();
+        let font_size = px(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 = text_system.line_wrapper(font.clone(), font_size).unwrap();
+        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, 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(px(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.update(|cx| {
+            let settings = SettingsStore::test(cx);
+            cx.set_global(settings);
+            theme::init(LoadThemes::JustBase, cx);
+        });
+    }
+
+    fn wrap_text(
+        unwrapped_text: &str,
+        wrap_width: Option<Pixels>,
+        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 🔗

@@ -3486,7 +3486,7 @@ impl Editor {
                         drop(context_menu);
                         this.discard_copilot_suggestion(cx);
                         cx.notify();
-                    } else if this.completion_tasks.is_empty() {
+                    } else if this.completion_tasks.len() <= 1 {
                         // 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.
@@ -8240,6 +8240,11 @@ impl Editor {
         self.style = Some(style);
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn style(&self) -> Option<&EditorStyle> {
+        self.style.as_ref()
+    }
+
     pub fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut AppContext) -> bool {
         self.display_map
             .update(cx, |map, cx| map.set_wrap_width(width, cx))

crates/editor2/src/editor_tests.rs 🔗

@@ -12,7 +12,7 @@ use futures::StreamExt;
 use gpui::{
     div,
     serde_json::{self, json},
-    Div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
+    Div, Flatten, Platform, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
 };
 use indoc::indoc;
 use language::{
@@ -36,121 +36,120 @@ use workspace::{
     NavigationEntry, ViewId,
 };
 
-// todo(finish edit tests)
-// #[gpui::test]
-// fn test_edit_events(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let buffer = cx.build_model(|cx| {
-//         let mut buffer = language::Buffer::new(0, cx.entity_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| {
-//             let view = cx.view().clone();
-//             cx.subscribe(&view, move |_, _, event, _| {
-//                 if matches!(event, Event::Edited | Event::BufferEdited) {
-//                     events.borrow_mut().push(("editor1", event.clone()));
-//                 }
-//             })
-//             .detach();
-//             Editor::for_buffer(buffer.clone(), None, cx)
-//         }
-//     });
-
-//     let editor2 = cx.add_window({
-//         let events = events.clone();
-//         |cx| {
-//             cx.subscribe(&cx.view().clone(), move |_, _, event, _| {
-//                 if matches!(event, Event::Edited | Event::BufferEdited) {
-//                     events.borrow_mut().push(("editor2", event.clone()));
-//                 }
-//             })
-//             .detach();
-//             Editor::for_buffer(buffer.clone(), None, 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),
-//         ]
-//     );
-
-//     // 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),
-//         ]
-//     );
-
-//     // 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),
-//         ]
-//     );
-
-//     // 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),
-//         ]
-//     );
-
-//     // 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),
-//         ]
-//     );
-
-//     // 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_edit_events(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let buffer = cx.build_model(|cx| {
+        let mut buffer = language::Buffer::new(0, cx.entity_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| {
+            let view = cx.view().clone();
+            cx.subscribe(&view, move |_, _, event: &EditorEvent, _| {
+                if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
+                    events.borrow_mut().push(("editor1", event.clone()));
+                }
+            })
+            .detach();
+            Editor::for_buffer(buffer.clone(), None, cx)
+        }
+    });
+
+    let editor2 = cx.add_window({
+        let events = events.clone();
+        |cx| {
+            cx.subscribe(&cx.view().clone(), move |_, _, event: &EditorEvent, _| {
+                if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
+                    events.borrow_mut().push(("editor2", event.clone()));
+                }
+            })
+            .detach();
+            Editor::for_buffer(buffer.clone(), None, 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", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // 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", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::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", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // 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", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // 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", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // 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", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // 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) {
@@ -515,123 +514,123 @@ fn test_clone(cx: &mut TestAppContext) {
 }
 
 //todo!(editor navigate)
-// #[gpui::test]
-// async fn test_navigation_history(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     use workspace::item::Item;
-
-//     let fs = FakeFs::new(cx.executor());
-//     let project = Project::test(fs, [], cx).await;
-//     let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
-//     let pane = workspace
-//         .update(cx, |workspace, _| workspace.active_pane().clone())
-//         .unwrap();
-
-//     workspace.update(cx, |v, cx| {
-//         cx.build_view(|cx| {
-//             let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
-//             let mut editor = build_editor(buffer.clone(), cx);
-//             let handle = cx.view();
-//             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.entity_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.entity_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(gpui::Point::<f32>::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(gpui::Point::<f32>::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),
-//                 gpui::Point::new(0., editor.max_point(cx).row() as f32)
-//             );
-
-//             editor
-//         })
-//     });
-// }
+#[gpui::test]
+async fn test_navigation_history(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    use workspace::item::Item;
+
+    let fs = FakeFs::new(cx.executor());
+    let project = Project::test(fs, [], cx).await;
+    let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
+    let pane = workspace
+        .update(cx, |workspace, _| workspace.active_pane().clone())
+        .unwrap();
+
+    workspace.update(cx, |v, cx| {
+        cx.build_view(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
+            let mut editor = build_editor(buffer.clone(), cx);
+            let handle = cx.view();
+            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.entity_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.entity_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(gpui::Point::<f32>::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(gpui::Point::<f32>::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),
+                gpui::Point::new(0., editor.max_point(cx).row() as f32)
+            );
+
+            editor
+        })
+    });
+}
 
 #[gpui::test]
 fn test_cancel(cx: &mut TestAppContext) {
@@ -959,55 +958,55 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
 }
 
 //todo!(finish editor tests)
-// #[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)
-//     });
-//     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_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)
+    });
+    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) {
@@ -1225,532 +1224,551 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
 }
 
 //todo!(finish editor tests)
-// #[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)
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.set_wrap_width(Some(140.0.into()), 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)]
-//         );
-//     });
-// }
-
-//todo!(simulate_resize)
-// #[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(gpui::Point::new(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(Point::new(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(),
-//             gpui::Point::new(0., 0.)
-//         );
-//         editor.scroll_screen(&ScrollAmount::Page(1.), cx);
-//         assert_eq!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(0., 3.)
-//         );
-//         editor.scroll_screen(&ScrollAmount::Page(1.), cx);
-//         assert_eq!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(0., 6.)
-//         );
-//         editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
-//         assert_eq!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(0., 3.)
-//         );
-
-//         editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
-//         assert_eq!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(0., 1.)
-//         );
-//         editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
-//         assert_eq!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(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(gpui::Point::new(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(),
-//             gpui::Point::new(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(),
-//             gpui::Point::new(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(),
-//             gpui::Point::new(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(),
-//             gpui::Point::new(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(gpui::Point::new(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) {
+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("one two three four", cx);
-        build_editor(buffer.clone(), cx)
+        let buffer = MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
+        build_editor(buffer, 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.set_wrap_width(Some(140.0.into()), cx);
+        assert_eq!(
+            view.display_text(cx),
+            "use one::{\n    two::three::\n    four::five\n};"
+        );
 
-    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),
-            ])
+            s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
         });
-        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, |_| {});
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
+        );
 
-    let view = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
-        build_editor(buffer.clone(), cx)
-    });
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
+        );
 
-    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.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
+        );
 
-        view.newline(&Newline, cx);
-        assert_eq!(view.text(cx), "aa\naa\n  \n    bb\n    bb\n");
+        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)]
+        );
     });
 }
 
+//todo!(simulate_resize)
 #[gpui::test]
-fn test_newline_with_old_selections(cx: &mut TestAppContext) {
+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 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),
-            ])
-        });
+    let line_height = cx.editor(|editor, cx| {
         editor
+            .style()
+            .unwrap()
+            .text
+            .line_height_in_pixels(cx.rem_size())
     });
+    cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
 
-    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()
+    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()
+            .unwrap()
+            .text
+            .line_height_in_pixels(cx.rem_size())
+    });
+    let window = cx.window;
+    cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
+
+    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(),
+            gpui::Point::new(0., 0.)
+        );
+        editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+        assert_eq!(
+            editor.snapshot(cx).scroll_position(),
+            gpui::Point::new(0., 3.)
+        );
+        editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+        assert_eq!(
+            editor.snapshot(cx).scroll_position(),
+            gpui::Point::new(0., 6.)
+        );
+        editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
+        assert_eq!(
+            editor.snapshot(cx).scroll_position(),
+            gpui::Point::new(0., 3.)
+        );
+
+        editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
+        assert_eq!(
+            editor.snapshot(cx).scroll_position(),
+            gpui::Point::new(0., 1.)
+        );
+        editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
+        assert_eq!(
+            editor.snapshot(cx).scroll_position(),
+            gpui::Point::new(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()
+            .unwrap()
+            .text
+            .line_height_in_pixels(cx.rem_size())
+    });
+    let window = cx.window;
+    cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
+
+    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(),
+            gpui::Point::new(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(),
+            gpui::Point::new(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(),
+            gpui::Point::new(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(),
+            gpui::Point::new(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()
+            .unwrap()
+            .text
+            .line_height_in_pixels(cx.rem_size())
+    });
+    let window = cx.window;
+    cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
+    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)
+    });
+
+    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)
+    });
+
+    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
+    });
+
+    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()

crates/editor2/src/element.rs 🔗

@@ -330,7 +330,7 @@ impl EditorElement {
         });
     }
 
-    fn modifiers_changed(
+    pub(crate) fn modifiers_changed(
         editor: &mut Editor,
         event: &ModifiersChangedEvent,
         cx: &mut ViewContext<Editor>,
@@ -3227,448 +3227,491 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
     (delta.pow(1.2) / 300.0).into()
 }
 
-// #[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(point(500., 500.), point(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(point(500., 500.), point(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(point(500., 500.), point(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 = Bounds::<Pixels>::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(point(editor_width, 500.), point(editor_width, 500.)),
-//                 editor,
-//                 cx,
-//             )
-//         });
-
-//         layout_state
-//             .position_map
-//             .line_layouts
-//             .iter()
-//             .map(|line_with_invisibles| &line_with_invisibles.invisibles)
-//             .flatten()
-//             .cloned()
-//             .collect()
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::{BlockDisposition, BlockProperties},
+        editor_tests::{init_test, update_test_language_settings},
+        Editor, MultiBuffer,
+    };
+    use gpui::{EmptyView, TestAppContext};
+    use language::language_settings;
+    use log::info;
+    use std::{num::NonZeroU32, sync::Arc};
+    use util::test::sample_text;
+
+    #[gpui::test]
+    fn test_shape_line_numbers(cx: &mut TestAppContext) {
+        init_test(cx, |_| {});
+        let window = cx.add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
+            Editor::new(EditorMode::Full, buffer, None, cx)
+        });
+
+        let editor = window.root(cx).unwrap();
+        let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
+        let element = EditorElement::new(&editor, style);
+
+        let layouts = window
+            .update(cx, |editor, cx| {
+                let snapshot = editor.snapshot(cx);
+                element
+                    .shape_line_numbers(
+                        0..6,
+                        &Default::default(),
+                        DisplayPoint::new(0, 0),
+                        false,
+                        &snapshot,
+                        cx,
+                    )
+                    .0
+            })
+            .unwrap();
+        assert_eq!(layouts.len(), 6);
+
+        let relative_rows = window
+            .update(cx, |editor, cx| {
+                let snapshot = editor.snapshot(cx);
+                element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3))
+            })
+            .unwrap();
+        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 = window
+            .update(cx, |editor, cx| {
+                let snapshot = editor.snapshot(cx);
+
+                element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1))
+            })
+            .unwrap();
+        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 = window
+            .update(cx, |editor, cx| {
+                let snapshot = editor.snapshot(cx);
+
+                element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6))
+            })
+            .unwrap();
+        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 window = cx.add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
+            Editor::new(EditorMode::Full, buffer, None, cx)
+        });
+        let editor = window.root(cx).unwrap();
+        let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
+        let mut element = EditorElement::new(&editor, style);
+
+        window
+            .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),
+                    ]);
+                });
+            })
+            .unwrap();
+        let state = cx
+            .update_window(window.into(), |_, cx| {
+                element.compute_layout(
+                    Bounds {
+                        origin: point(px(500.), px(500.)),
+                        size: size(px(500.), px(500.)),
+                    },
+                    cx,
+                )
+            })
+            .unwrap();
+
+        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 window = 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, cx)
+        });
+        let editor = window.root(cx).unwrap();
+        let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
+        let mut element = EditorElement::new(&editor, style);
+        let state = window.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),
+                ]);
+            });
+        });
+
+        let state = cx
+            .update_window(window.into(), |_, cx| {
+                element.compute_layout(
+                    Bounds {
+                        origin: point(px(500.), px(500.)),
+                        size: size(px(500.), px(500.)),
+                    },
+                    cx,
+                )
+            })
+            .unwrap();
+        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));
+        dbg!("Hi");
+        // 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 window = cx.add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("", cx);
+            Editor::new(EditorMode::Full, buffer, None, cx)
+        });
+        let editor = window.root(cx).unwrap();
+        let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
+        window
+            .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(|_| div().into_any()),
+                    }],
+                    None,
+                    cx,
+                );
+
+                // Blur the editor so that it displays placeholder text.
+                cx.blur();
+            })
+            .unwrap();
+
+        let mut element = EditorElement::new(&editor, style);
+        let mut state = cx
+            .update_window(window.into(), |_, cx| {
+                element.compute_layout(
+                    Bounds {
+                        origin: point(px(500.), px(500.)),
+                        size: size(px(500.), px(500.)),
+                    },
+                    cx,
+                )
+            })
+            .unwrap();
+        let size = state.position_map.size;
+
+        assert_eq!(state.position_map.line_layouts.len(), 4);
+        assert_eq!(
+            state
+                .line_numbers
+                .iter()
+                .map(Option::is_some)
+                .collect::<Vec<_>>(),
+            &[false, false, false, true]
+        );
+
+        // Don't panic.
+        let bounds = Bounds::<Pixels>::new(Default::default(), size);
+        cx.update_window(window.into(), |_, cx| {
+            element.paint(bounds, &mut (), cx);
+        })
+        .unwrap()
+    }
+
+    #[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, px(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",
+                px(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,
+                px(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: Pixels,
+    ) -> Vec<Invisible> {
+        info!(
+            "Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
+            editor_width.0
+        );
+        let window = cx.add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&input_text, cx);
+            Editor::new(editor_mode, buffer, None, cx)
+        });
+        let editor = window.root(cx).unwrap();
+        let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
+        let mut element = EditorElement::new(&editor, style);
+        window
+            .update(cx, |editor, cx| {
+                editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
+                editor.set_wrap_width(Some(editor_width), cx);
+            })
+            .unwrap();
+        let layout_state = cx
+            .update_window(window.into(), |_, cx| {
+                element.compute_layout(
+                    Bounds {
+                        origin: point(px(500.), px(500.)),
+                        size: size(px(500.), px(500.)),
+                    },
+                    cx,
+                )
+            })
+            .unwrap();
+
+        layout_state
+            .position_map
+            .line_layouts
+            .iter()
+            .map(|line_with_invisibles| &line_with_invisibles.invisibles)
+            .flatten()
+            .cloned()
+            .collect()
+    }
+}
 
 pub fn register_action<T: Action>(
     view: &View<Editor>,

crates/editor2/src/git.rs 🔗

@@ -88,195 +88,195 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
     }
 }
 
-// #[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, |_| {});
+#[cfg(test)]
+mod tests {
+    use crate::editor_tests::init_test;
+    use crate::Point;
+    use gpui::{Context, 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;
+        let fs = FakeFs::new(cx.background_executor.clone());
+        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 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,
-//             );
-//         });
+        // 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();
+        cx.background_executor.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 multibuffer = cx.build_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));
+        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()
-//         );
+        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),
-//         ];
+        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(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(),
-//         );
-//     }
-// }
+        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 🔗

@@ -5,7 +5,7 @@ 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);
+    editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
 
     let newest_selection = editor.selections.newest::<usize>(cx);
     // Don't highlight brackets if the selection isn't empty
@@ -30,109 +30,109 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
     }
 }
 
-// #[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};
+#[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, |_| {});
+    #[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;
+        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);
-//             }
-//         "#});
+        // 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«)»;
+            }
+        "#});
 
-//         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);
-//             }
-//         "#});
+        // 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);
-//             }
-//         "#});
-//     }
-// }
+        // 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/inlay_hint_cache.rs 🔗

@@ -2432,13 +2432,13 @@ pub mod tests {
         let language = Arc::new(language);
         let fs = FakeFs::new(cx.background_executor.clone());
         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;
+                "/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))
@@ -2598,24 +2598,22 @@ pub mod tests {
         cx.executor().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(),
-                // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther
-                // (or renders less?) note that tests below pass
-                "main hint #4".to_string(),
-                "main hint #5".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");
-        });
+                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(),
+                ];
+                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| {
@@ -2630,23 +2628,23 @@ pub mod tests {
         });
         cx.executor().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");
-        });
+                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| {
@@ -2658,26 +2656,26 @@ pub mod tests {
         ));
         cx.executor().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()
-        }).unwrap();
+                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()
+            }).unwrap();
 
         editor.update(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::Next), cx, |s| {
@@ -2686,30 +2684,31 @@ pub mod tests {
         });
         cx.executor().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");
-        });
+                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)])
+                // TODO if this gets set to hint boundary (e.g. 56) we sometimes get an extra cache version bump, why?
+                s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
             });
             editor.handle_input("++++more text++++", cx);
         });
@@ -2729,15 +2728,15 @@ pub mod tests {
                 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"
+    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"
+            assert_eq!(
+                current_cache_version,
+                last_scroll_update_version + expected_hints.len(),
+                "We should have updated cache N times == N of new hints arrived (separately from each excerpt)"
             );
         });
     }
@@ -608,671 +608,672 @@ fn go_to_fetched_definition_of_kind(
     }
 }
 
-// #[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;
-//             }
-//         "});
-//     }
-// }
+#[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::{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.background_executor.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| {
+            crate::element::EditorElement::modifiers_changed(
+                editor,
+                &ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        command: 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.background_executor.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.background_executor.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| {
+            crate::element::EditorElement::modifiers_changed(editor, &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.background_executor.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.background_executor.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.background_executor.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| {
+            crate::element::EditorElement::modifiers_changed(
+                editor,
+                &ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        command: true,
+                        ..Default::default()
+                    },
+                },
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.background_executor.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(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.background_executor.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.background_executor.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.background_executor.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.background_executor.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.background_executor.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.background_executor.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.background_executor.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.background_executor.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| {
+            crate::element::EditorElement::modifiers_changed(
+                editor,
+                &ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        command: 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| {
+            crate::element::EditorElement::modifiers_changed(
+                editor,
+                &ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        command: true,
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                cx,
+            );
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                hint_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor.run_until_parked();
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
+        });
+        cx.background_executor.run_until_parked();
+        cx.assert_editor_state(indoc! {"
+                struct «TestStructˇ»;
+
+                fn main() {
+                    let variable = TestStruct;
+                }
+            "});
+    }
+}

crates/editor2/src/mouse_context_menu.rs 🔗

@@ -68,42 +68,43 @@ pub fn deploy_context_menu(
     cx.notify();
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
-//     use indoc::indoc;
+#[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, |_| {});
+    #[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;
+        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.set_state(indoc! {"
+            fn teˇst() {
+                do_work();
+            }
+        "});
+        let point = cx.display_point(indoc! {"
+            fn test() {
+                do_wˇork();
+            }
+        "});
+        cx.editor(|editor, app| assert!(editor.mouse_context_menu.is_none()));
+        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()));
-//     }
-// }
+        cx.assert_editor_state(indoc! {"
+            fn test() {
+                do_wˇork();
+            }
+        "});
+        cx.editor(|editor, app| assert!(editor.mouse_context_menu.is_some()));
+    }
+}

crates/editor2/src/movement.rs 🔗

@@ -452,483 +452,475 @@ pub fn split_display_range_by_lines(
     result
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::{
-//         display_map::Inlay,
-//         test::{},
-//         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);
-//     }
-// }
+#[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 gpui::{font, Context as _};
+    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 font = font("Helvetica");
+        let font_size = px(14.0);
+        let buffer = MultiBuffer::build_simple(input_text, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let display_map =
+            cx.build_model(|cx| DisplayMap::new(buffer, font, 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.update(cx, |editor, cx| editor.text_layout_details(cx));
+
+            let font = font("Helvetica");
+
+            let buffer = cx
+                .build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn"));
+            let multibuffer = cx.build_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.build_model(|cx| DisplayMap::new(multibuffer, font, px(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_display_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.0),
+                    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_display_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.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 3),
+                    SelectionGoal::HorizontalPosition(col_4_x.0)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(2, 3),
+                    SelectionGoal::HorizontalPosition(col_4_x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_4_x.0)
+                ),
+            );
+
+            let col_5_x =
+                snapshot.x_for_display_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.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_5_x.0)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_5_x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(6, 5),
+                    SelectionGoal::HorizontalPosition(col_5_x.0)
+                ),
+            );
+
+            let max_point_x =
+                snapshot.x_for_display_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.0)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x.0)
+                ),
+            );
+        });
+    }
+
+    fn init_test(cx: &mut gpui::AppContext) {
+        let settings_store = SettingsStore::test(cx);
+        cx.set_global(settings_store);
+        theme::init(theme::LoadThemes::JustBase, cx);
+        language::init(cx);
+        crate::init(cx);
+        Project::init_settings(cx);
+    }
+}

crates/gpui2/src/app.rs 🔗

@@ -358,7 +358,7 @@ impl AppContext {
     {
         let entity_id = entity.entity_id();
         let handle = entity.downgrade();
-        self.observers.insert(
+        let (subscription, activate) = self.observers.insert(
             entity_id,
             Box::new(move |cx| {
                 if let Some(handle) = E::upgrade_from(&handle) {
@@ -367,7 +367,9 @@ impl AppContext {
                     false
                 }
             }),
-        )
+        );
+        self.defer(move |_| activate());
+        subscription
     }
 
     pub fn subscribe<T, E, Evt>(
@@ -398,8 +400,7 @@ impl AppContext {
     {
         let entity_id = entity.entity_id();
         let entity = entity.downgrade();
-
-        self.event_listeners.insert(
+        let (subscription, activate) = self.event_listeners.insert(
             entity_id,
             (
                 TypeId::of::<Evt>(),
@@ -412,7 +413,9 @@ impl AppContext {
                     }
                 }),
             ),
-        )
+        );
+        self.defer(move |_| activate());
+        subscription
     }
 
     pub fn windows(&self) -> Vec<AnyWindowHandle> {
@@ -873,13 +876,15 @@ impl AppContext {
         &mut self,
         mut f: impl FnMut(&mut Self) + 'static,
     ) -> Subscription {
-        self.global_observers.insert(
+        let (subscription, activate) = self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| {
                 f(cx);
                 true
             }),
-        )
+        );
+        self.defer(move |_| activate());
+        subscription
     }
 
     /// Move the global of the given type to the stack.
@@ -903,7 +908,7 @@ impl AppContext {
         &mut self,
         on_new: impl 'static + Fn(&mut V, &mut ViewContext<V>),
     ) -> Subscription {
-        self.new_view_observers.insert(
+        let (subscription, activate) = self.new_view_observers.insert(
             TypeId::of::<V>(),
             Box::new(move |any_view: AnyView, cx: &mut WindowContext| {
                 any_view
@@ -913,7 +918,9 @@ impl AppContext {
                         on_new(view_state, cx);
                     })
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn observe_release<E, T>(
@@ -925,13 +932,15 @@ impl AppContext {
         E: Entity<T>,
         T: 'static,
     {
-        self.release_listeners.insert(
+        let (subscription, activate) = self.release_listeners.insert(
             handle.entity_id(),
             Box::new(move |entity, cx| {
                 let entity = entity.downcast_mut().expect("invalid entity type");
                 on_release(entity, cx)
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
@@ -996,13 +1005,15 @@ impl AppContext {
     where
         Fut: 'static + Future<Output = ()>,
     {
-        self.quit_observers.insert(
+        let (subscription, activate) = self.quit_observers.insert(
             (),
             Box::new(move |cx| {
                 let future = on_quit(cx);
                 async move { future.await }.boxed_local()
             }),
-        )
+        );
+        activate();
+        subscription
     }
 }
 

crates/gpui2/src/app/model_context.rs 🔗

@@ -88,13 +88,15 @@ impl<'a, T: 'static> ModelContext<'a, T> {
     where
         T: 'static,
     {
-        self.app.release_listeners.insert(
+        let (subscription, activate) = self.app.release_listeners.insert(
             self.model_state.entity_id,
             Box::new(move |this, cx| {
                 let this = this.downcast_mut().expect("invalid entity type");
                 on_release(this, cx);
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn observe_release<T2, E>(
@@ -109,7 +111,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
     {
         let entity_id = entity.entity_id();
         let this = self.weak_model();
-        self.app.release_listeners.insert(
+        let (subscription, activate) = self.app.release_listeners.insert(
             entity_id,
             Box::new(move |entity, cx| {
                 let entity = entity.downcast_mut().expect("invalid entity type");
@@ -117,7 +119,9 @@ impl<'a, T: 'static> ModelContext<'a, T> {
                     this.update(cx, |this, cx| on_release(this, entity, cx));
                 }
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn observe_global<G: 'static>(
@@ -128,10 +132,12 @@ impl<'a, T: 'static> ModelContext<'a, T> {
         T: 'static,
     {
         let handle = self.weak_model();
-        self.global_observers.insert(
+        let (subscription, activate) = self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()),
-        )
+        );
+        self.defer(move |_| activate());
+        subscription
     }
 
     pub fn on_app_quit<Fut>(
@@ -143,7 +149,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
         T: 'static,
     {
         let handle = self.weak_model();
-        self.app.quit_observers.insert(
+        let (subscription, activate) = self.app.quit_observers.insert(
             (),
             Box::new(move |cx| {
                 let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
@@ -154,7 +160,9 @@ impl<'a, T: 'static> ModelContext<'a, T> {
                 }
                 .boxed_local()
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn notify(&mut self) {

crates/gpui2/src/app/test_context.rs 🔗

@@ -1,13 +1,13 @@
 use crate::{
     div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
-    BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
-    KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher,
-    TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext, VisualContext, WindowContext,
-    WindowHandle, WindowOptions,
+    BackgroundExecutor, Bounds, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
+    KeyDownEvent, Keystroke, Model, ModelContext, Pixels, PlatformWindow, Point, Render, Result,
+    Size, Task, TestDispatcher, TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext,
+    VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
-use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
+use std::{future::Future, mem, ops::Deref, rc::Rc, sync::Arc, time::Duration};
 
 #[derive(Clone)]
 pub struct TestAppContext {
@@ -170,6 +170,45 @@ impl TestAppContext {
         self.test_platform.has_pending_prompt()
     }
 
+    pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
+        let (mut handlers, scale_factor) = self
+            .app
+            .borrow_mut()
+            .update_window(window_handle, |_, cx| {
+                let platform_window = cx.window.platform_window.as_test().unwrap();
+                let scale_factor = platform_window.scale_factor();
+                match &mut platform_window.bounds {
+                    WindowBounds::Fullscreen | WindowBounds::Maximized => {
+                        platform_window.bounds = WindowBounds::Fixed(Bounds {
+                            origin: Point::default(),
+                            size: size.map(|pixels| f64::from(pixels).into()),
+                        });
+                    }
+                    WindowBounds::Fixed(bounds) => {
+                        bounds.size = size.map(|pixels| f64::from(pixels).into());
+                    }
+                }
+
+                (
+                    mem::take(&mut platform_window.handlers.lock().resize),
+                    scale_factor,
+                )
+            })
+            .unwrap();
+
+        for handler in &mut handlers {
+            handler(size, scale_factor);
+        }
+
+        self.app
+            .borrow_mut()
+            .update_window(window_handle, |_, cx| {
+                let platform_window = cx.window.platform_window.as_test().unwrap();
+                platform_window.handlers.lock().resize = handlers;
+            })
+            .unwrap();
+    }
+
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
     where
         Fut: Future<Output = R> + 'static,
@@ -343,12 +382,15 @@ impl TestAppContext {
         use smol::future::FutureExt as _;
 
         async {
-            while notifications.next().await.is_some() {
+            loop {
                 if model.update(self, &mut predicate) {
                     return Ok(());
                 }
+
+                if notifications.next().await.is_none() {
+                    bail!("model dropped")
+                }
             }
-            bail!("model dropped")
         }
         .race(timer.map(|_| Err(anyhow!("condition timed out"))))
         .await

crates/gpui2/src/executor.rs 🔗

@@ -128,11 +128,19 @@ impl BackgroundExecutor {
     #[cfg(any(test, feature = "test-support"))]
     #[track_caller]
     pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
-        self.block_internal(false, future)
+        if let Ok(value) = self.block_internal(false, future, usize::MAX) {
+            value
+        } else {
+            unreachable!()
+        }
     }
 
     pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
-        self.block_internal(true, future)
+        if let Ok(value) = self.block_internal(true, future, usize::MAX) {
+            value
+        } else {
+            unreachable!()
+        }
     }
 
     #[track_caller]
@@ -140,7 +148,8 @@ impl BackgroundExecutor {
         &self,
         background_only: bool,
         future: impl Future<Output = R>,
-    ) -> R {
+        mut max_ticks: usize,
+    ) -> Result<R, ()> {
         pin_mut!(future);
         let unparker = self.dispatcher.unparker();
         let awoken = Arc::new(AtomicBool::new(false));
@@ -156,8 +165,13 @@ impl BackgroundExecutor {
 
         loop {
             match future.as_mut().poll(&mut cx) {
-                Poll::Ready(result) => return result,
+                Poll::Ready(result) => return Ok(result),
                 Poll::Pending => {
+                    if max_ticks == 0 {
+                        return Err(());
+                    }
+                    max_ticks -= 1;
+
                     if !self.dispatcher.tick(background_only) {
                         if awoken.swap(false, SeqCst) {
                             continue;
@@ -192,16 +206,25 @@ impl BackgroundExecutor {
             return Err(future);
         }
 
+        #[cfg(any(test, feature = "test-support"))]
+        let max_ticks = self
+            .dispatcher
+            .as_test()
+            .map_or(usize::MAX, |dispatcher| dispatcher.gen_block_on_ticks());
+        #[cfg(not(any(test, feature = "test-support")))]
+        let max_ticks = usize::MAX;
+
         let mut timer = self.timer(duration).fuse();
+
         let timeout = async {
             futures::select_biased! {
                 value = future => Ok(value),
                 _ = timer => Err(()),
             }
         };
-        match self.block(timeout) {
-            Ok(value) => Ok(value),
-            Err(_) => Err(future),
+        match self.block_internal(true, timeout, max_ticks) {
+            Ok(Ok(value)) => Ok(value),
+            _ => Err(future),
         }
     }
 
@@ -281,6 +304,11 @@ impl BackgroundExecutor {
     pub fn is_main_thread(&self) -> bool {
         self.dispatcher.is_main_thread()
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
+        self.dispatcher.as_test().unwrap().set_block_on_ticks(range);
+    }
 }
 
 impl ForegroundExecutor {

crates/gpui2/src/gpui2.rs 🔗

@@ -21,7 +21,7 @@ mod subscription;
 mod svg_renderer;
 mod taffy;
 #[cfg(any(test, feature = "test-support"))]
-mod test;
+pub mod test;
 mod text_system;
 mod util;
 mod view;

crates/gpui2/src/platform.rs 🔗

@@ -44,7 +44,7 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
     Rc::new(MacPlatform::new())
 }
 
-pub(crate) trait Platform: 'static {
+pub trait Platform: 'static {
     fn background_executor(&self) -> BackgroundExecutor;
     fn foreground_executor(&self) -> ForegroundExecutor;
     fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
@@ -128,7 +128,7 @@ impl Debug for DisplayId {
 
 unsafe impl Send for DisplayId {}
 
-pub(crate) trait PlatformWindow {
+pub trait PlatformWindow {
     fn bounds(&self) -> WindowBounds;
     fn content_size(&self) -> Size<Pixels>;
     fn scale_factor(&self) -> f32;
@@ -160,7 +160,7 @@ pub(crate) trait PlatformWindow {
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
 
     #[cfg(any(test, feature = "test-support"))]
-    fn as_test(&self) -> Option<&TestWindow> {
+    fn as_test(&mut self) -> Option<&mut TestWindow> {
         None
     }
 }

crates/gpui2/src/platform/test/dispatcher.rs 🔗

@@ -7,6 +7,7 @@ use parking_lot::Mutex;
 use rand::prelude::*;
 use std::{
     future::Future,
+    ops::RangeInclusive,
     pin::Pin,
     sync::Arc,
     task::{Context, Poll},
@@ -36,6 +37,7 @@ struct TestDispatcherState {
     allow_parking: bool,
     waiting_backtrace: Option<Backtrace>,
     deprioritized_task_labels: HashSet<TaskLabel>,
+    block_on_ticks: RangeInclusive<usize>,
 }
 
 impl TestDispatcher {
@@ -53,6 +55,7 @@ impl TestDispatcher {
             allow_parking: false,
             waiting_backtrace: None,
             deprioritized_task_labels: Default::default(),
+            block_on_ticks: 0..=1000,
         };
 
         TestDispatcher {
@@ -82,8 +85,8 @@ impl TestDispatcher {
     }
 
     pub fn simulate_random_delay(&self) -> impl 'static + Send + Future<Output = ()> {
-        pub struct YieldNow {
-            count: usize,
+        struct YieldNow {
+            pub(crate) count: usize,
         }
 
         impl Future for YieldNow {
@@ -142,6 +145,16 @@ impl TestDispatcher {
     pub fn rng(&self) -> StdRng {
         self.state.lock().random.clone()
     }
+
+    pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
+        self.state.lock().block_on_ticks = range;
+    }
+
+    pub fn gen_block_on_ticks(&self) -> usize {
+        let mut lock = self.state.lock();
+        let block_on_ticks = lock.block_on_ticks.clone();
+        lock.random.gen_range(block_on_ticks)
+    }
 }
 
 impl Clone for TestDispatcher {

crates/gpui2/src/platform/test/window.rs 🔗

@@ -19,7 +19,7 @@ pub(crate) struct TestWindowHandlers {
 }
 
 pub struct TestWindow {
-    bounds: WindowBounds,
+    pub(crate) bounds: WindowBounds,
     current_scene: Mutex<Option<Scene>>,
     display: Rc<dyn PlatformDisplay>,
     pub(crate) window_title: Option<String>,
@@ -170,7 +170,7 @@ impl PlatformWindow for TestWindow {
         self.sprite_atlas.clone()
     }
 
-    fn as_test(&self) -> Option<&TestWindow> {
+    fn as_test(&mut self) -> Option<&mut TestWindow> {
         Some(self)
     }
 }

crates/gpui2/src/scene.rs 🔗

@@ -198,7 +198,7 @@ impl SceneBuilder {
     }
 }
 
-pub(crate) struct Scene {
+pub struct Scene {
     pub shadows: Vec<Shadow>,
     pub quads: Vec<Quad>,
     pub paths: Vec<Path<ScaledPixels>>,
@@ -214,7 +214,7 @@ impl Scene {
         &self.paths
     }
 
-    pub fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
+    pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
         BatchIterator {
             shadows: &self.shadows,
             shadows_start: 0,

crates/gpui2/src/style.rs 🔗

@@ -208,8 +208,9 @@ impl TextStyle {
         }
     }
 
+    /// Returns the rounded line height in pixels.
     pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels {
-        self.line_height.to_pixels(self.font_size, rem_size)
+        self.line_height.to_pixels(self.font_size, rem_size).round()
     }
 
     pub fn to_run(&self, len: usize) -> TextRun {

crates/gpui2/src/subscription.rs 🔗

@@ -1,6 +1,6 @@
 use collections::{BTreeMap, BTreeSet};
 use parking_lot::Mutex;
-use std::{fmt::Debug, mem, sync::Arc};
+use std::{cell::Cell, fmt::Debug, mem, rc::Rc, sync::Arc};
 use util::post_inc;
 
 pub(crate) struct SubscriberSet<EmitterKey, Callback>(
@@ -14,11 +14,16 @@ impl<EmitterKey, Callback> Clone for SubscriberSet<EmitterKey, Callback> {
 }
 
 struct SubscriberSetState<EmitterKey, Callback> {
-    subscribers: BTreeMap<EmitterKey, Option<BTreeMap<usize, Callback>>>,
+    subscribers: BTreeMap<EmitterKey, Option<BTreeMap<usize, Subscriber<Callback>>>>,
     dropped_subscribers: BTreeSet<(EmitterKey, usize)>,
     next_subscriber_id: usize,
 }
 
+struct Subscriber<Callback> {
+    active: Rc<Cell<bool>>,
+    callback: Callback,
+}
+
 impl<EmitterKey, Callback> SubscriberSet<EmitterKey, Callback>
 where
     EmitterKey: 'static + Ord + Clone + Debug,
@@ -32,16 +37,33 @@ where
         })))
     }
 
-    pub fn insert(&self, emitter_key: EmitterKey, callback: Callback) -> Subscription {
+    /// Inserts a new `[Subscription]` for the given `emitter_key`. By default, subscriptions
+    /// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`.
+    /// This method returns a tuple of a `[Subscription]` and an `impl FnOnce`, and you can use the latter
+    /// to activate the `[Subscription]`.
+    #[must_use]
+    pub fn insert(
+        &self,
+        emitter_key: EmitterKey,
+        callback: Callback,
+    ) -> (Subscription, impl FnOnce()) {
+        let active = Rc::new(Cell::new(false));
         let mut lock = self.0.lock();
         let subscriber_id = post_inc(&mut lock.next_subscriber_id);
         lock.subscribers
             .entry(emitter_key.clone())
             .or_default()
             .get_or_insert_with(|| Default::default())
-            .insert(subscriber_id, callback);
+            .insert(
+                subscriber_id,
+                Subscriber {
+                    active: active.clone(),
+                    callback,
+                },
+            );
         let this = self.0.clone();
-        Subscription {
+
+        let subscription = Subscription {
             unsubscribe: Some(Box::new(move || {
                 let mut lock = this.lock();
                 let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) else {
@@ -63,7 +85,8 @@ where
                 lock.dropped_subscribers
                     .insert((emitter_key, subscriber_id));
             })),
-        }
+        };
+        (subscription, move || active.set(true))
     }
 
     pub fn remove(&self, emitter: &EmitterKey) -> impl IntoIterator<Item = Callback> {
@@ -73,6 +96,13 @@ where
             .map(|s| s.into_values())
             .into_iter()
             .flatten()
+            .filter_map(|subscriber| {
+                if subscriber.active.get() {
+                    Some(subscriber.callback)
+                } else {
+                    None
+                }
+            })
     }
 
     /// Call the given callback for each subscriber to the given emitter.
@@ -91,7 +121,13 @@ where
             return;
         };
 
-        subscribers.retain(|_, callback| f(callback));
+        subscribers.retain(|_, subscriber| {
+            if subscriber.active.get() {
+                f(&mut subscriber.callback)
+            } else {
+                true
+            }
+        });
         let mut lock = self.0.lock();
 
         // Add any new subscribers that were added while invoking the callback.

crates/gpui2/src/test.rs 🔗

@@ -1,5 +1,7 @@
-use crate::TestDispatcher;
+use crate::{Entity, Subscription, TestAppContext, TestDispatcher};
+use futures::StreamExt as _;
 use rand::prelude::*;
+use smol::channel;
 use std::{
     env,
     panic::{self, RefUnwindSafe},
@@ -49,3 +51,30 @@ pub fn run_test(
         }
     }
 }
+
+pub struct Observation<T> {
+    rx: channel::Receiver<T>,
+    _subscription: Subscription,
+}
+
+impl<T: 'static> futures::Stream for Observation<T> {
+    type Item = T;
+
+    fn poll_next(
+        mut self: std::pin::Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+    ) -> std::task::Poll<Option<Self::Item>> {
+        self.rx.poll_next_unpin(cx)
+    }
+}
+
+pub fn observe<T: 'static>(entity: &impl Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
+    let (tx, rx) = smol::channel::unbounded();
+    let _subscription = cx.update(|cx| {
+        cx.observe(entity, move |_, _| {
+            let _ = smol::block_on(tx.send(()));
+        })
+    });
+
+    Observation { rx, _subscription }
+}

crates/gpui2/src/window.rs 🔗

@@ -490,7 +490,7 @@ impl<'a> WindowContext<'a> {
         let entity_id = entity.entity_id();
         let entity = entity.downgrade();
         let window_handle = self.window.handle;
-        self.app.event_listeners.insert(
+        let (subscription, activate) = self.app.event_listeners.insert(
             entity_id,
             (
                 TypeId::of::<Evt>(),
@@ -508,7 +508,9 @@ impl<'a> WindowContext<'a> {
                         .unwrap_or(false)
                 }),
             ),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     /// Create an `AsyncWindowContext`, which has a static lifetime and can be held across
@@ -1458,10 +1460,12 @@ impl<'a> WindowContext<'a> {
         f: impl Fn(&mut WindowContext<'_>) + 'static,
     ) -> Subscription {
         let window_handle = self.window.handle;
-        self.global_observers.insert(
+        let (subscription, activate) = self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| window_handle.update(cx, |_, cx| f(cx)).is_ok()),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     pub fn activate_window(&self) {
@@ -2122,7 +2126,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         let entity_id = entity.entity_id();
         let entity = entity.downgrade();
         let window_handle = self.window.handle;
-        self.app.observers.insert(
+        let (subscription, activate) = self.app.observers.insert(
             entity_id,
             Box::new(move |cx| {
                 window_handle
@@ -2136,7 +2140,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                     })
                     .unwrap_or(false)
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     pub fn subscribe<V2, E, Evt>(
@@ -2153,7 +2159,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         let entity_id = entity.entity_id();
         let handle = entity.downgrade();
         let window_handle = self.window.handle;
-        self.app.event_listeners.insert(
+        let (subscription, activate) = self.app.event_listeners.insert(
             entity_id,
             (
                 TypeId::of::<Evt>(),
@@ -2171,7 +2177,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                         .unwrap_or(false)
                 }),
             ),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     pub fn on_release(
@@ -2179,13 +2187,15 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         on_release: impl FnOnce(&mut V, &mut WindowContext) + 'static,
     ) -> Subscription {
         let window_handle = self.window.handle;
-        self.app.release_listeners.insert(
+        let (subscription, activate) = self.app.release_listeners.insert(
             self.view.model.entity_id,
             Box::new(move |this, cx| {
                 let this = this.downcast_mut().expect("invalid entity type");
                 let _ = window_handle.update(cx, |_, cx| on_release(this, cx));
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn observe_release<V2, E>(
@@ -2201,7 +2211,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         let view = self.view().downgrade();
         let entity_id = entity.entity_id();
         let window_handle = self.window.handle;
-        self.app.release_listeners.insert(
+        let (subscription, activate) = self.app.release_listeners.insert(
             entity_id,
             Box::new(move |entity, cx| {
                 let entity = entity.downcast_mut().expect("invalid entity type");
@@ -2209,7 +2219,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                     view.update(cx, |this, cx| on_release(this, entity, cx))
                 });
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn notify(&mut self) {
@@ -2224,10 +2236,12 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
     ) -> Subscription {
         let view = self.view.downgrade();
-        self.window.bounds_observers.insert(
+        let (subscription, activate) = self.window.bounds_observers.insert(
             (),
             Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn observe_window_activation(
@@ -2235,10 +2249,12 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
     ) -> Subscription {
         let view = self.view.downgrade();
-        self.window.activation_observers.insert(
+        let (subscription, activate) = self.window.activation_observers.insert(
             (),
             Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
-        )
+        );
+        activate();
+        subscription
     }
 
     /// Register a listener to be called when the given focus handle receives focus.
@@ -2251,7 +2267,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        self.window.focus_listeners.insert(
+        let (subscription, activate) = self.window.focus_listeners.insert(
             (),
             Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
@@ -2261,7 +2277,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                 })
                 .is_ok()
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     /// Register a listener to be called when the given focus handle or one of its descendants receives focus.
@@ -2274,7 +2292,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        self.window.focus_listeners.insert(
+        let (subscription, activate) = self.window.focus_listeners.insert(
             (),
             Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
@@ -2288,7 +2306,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                 })
                 .is_ok()
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     /// Register a listener to be called when the given focus handle loses focus.
@@ -2301,7 +2321,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        self.window.focus_listeners.insert(
+        let (subscription, activate) = self.window.focus_listeners.insert(
             (),
             Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
@@ -2311,7 +2331,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                 })
                 .is_ok()
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     /// Register a listener to be called when the given focus handle or one of its descendants loses focus.
@@ -2324,7 +2346,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        self.window.focus_listeners.insert(
+        let (subscription, activate) = self.window.focus_listeners.insert(
             (),
             Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
@@ -2338,7 +2360,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                 })
                 .is_ok()
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     pub fn spawn<Fut, R>(
@@ -2369,14 +2393,16 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let window_handle = self.window.handle;
         let view = self.view().downgrade();
-        self.global_observers.insert(
+        let (subscription, activate) = self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| {
                 window_handle
                     .update(cx, |_, cx| view.update(cx, |view, cx| f(view, cx)).is_ok())
                     .unwrap_or(false)
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     pub fn on_mouse_event<Event: 'static>(