Cargo.lock 🔗
@@ -2112,7 +2112,7 @@ dependencies = [
"lsp2",
"node_runtime",
"parking_lot 0.11.2",
- "rpc",
+ "rpc2",
"serde",
"serde_derive",
"settings2",
Piotr Osiewicz created
Release Notes:
- N/A
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(-)
@@ -2112,7 +2112,7 @@ dependencies = [
"lsp2",
"node_runtime",
"parking_lot 0.11.2",
- "rpc",
+ "rpc2",
"serde",
"serde_derive",
"settings2",
@@ -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"] }
@@ -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!()
+ }
+ }
+}
@@ -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);
+ });
+ }
+}
@@ -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)
+ }
+ }
+}
@@ -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
+ );
+ }
+ }
+ }
+}
@@ -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))
@@ -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()
@@ -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>,
@@ -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(),
+ );
+ }
+}
@@ -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);
+ }
+ "#});
+ }
+}
@@ -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;
+ }
+ "});
+ }
+}
@@ -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()));
+ }
+}
@@ -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);
+ }
+}
@@ -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
}
}
@@ -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) {
@@ -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
@@ -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 {
@@ -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;
@@ -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
}
}
@@ -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 {
@@ -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)
}
}
@@ -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,
@@ -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 {
@@ -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.
@@ -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 }
+}
@@ -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>(