diff --git a/Cargo.lock b/Cargo.lock index 49f37fb0429b0d2ddd44b32099e1a1544adc6fbc..97653e124a49e3f41fdfe1d9a13682960a7777d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2012,7 +2012,7 @@ dependencies = [ "serde_derive", "settings2", "smol", - "theme", + "theme2", "util", ] @@ -2768,7 +2768,6 @@ dependencies = [ "copilot2", "ctor", "db2", - "drag_and_drop", "env_logger 0.9.3", "futures 0.3.28", "fuzzy2", diff --git a/crates/client2/src/test.rs b/crates/client2/src/test.rs index 54627991038f18eded4ded13e83bbcdf1bc3979f..086cbd570ec0f8d9d5108dc889a589b0b41572e0 100644 --- a/crates/client2/src/test.rs +++ b/crates/client2/src/test.rs @@ -36,7 +36,7 @@ impl FakeServer { peer: Peer::new(0), state: Default::default(), user_id: client_user_id, - executor: cx.executor().clone(), + executor: cx.executor(), }; client diff --git a/crates/collab2/src/db/tests/db_tests.rs b/crates/collab2/src/db/tests/db_tests.rs index 98d1fee8fa03e382cacc7f9eb74863074aa80768..1f825efd74583d6754e762e54e1d5faa3b082889 100644 --- a/crates/collab2/src/db/tests/db_tests.rs +++ b/crates/collab2/src/db/tests/db_tests.rs @@ -510,7 +510,7 @@ fn test_fuzzy_like_string() { #[gpui::test] async fn test_fuzzy_search_users(cx: &mut TestAppContext) { - let test_db = TestDb::postgres(cx.executor().clone()); + let test_db = TestDb::postgres(cx.executor()); let db = test_db.db(); for (i, github_login) in [ "California", diff --git a/crates/collab2/src/tests.rs b/crates/collab2/src/tests.rs index cb25856551d52000927aa23a7608bbfaf39734a8..a669f260dba91038d4d834d72a03be999823c928 100644 --- a/crates/collab2/src/tests.rs +++ b/crates/collab2/src/tests.rs @@ -4,6 +4,7 @@ use gpui::{Model, TestAppContext}; mod channel_buffer_tests; mod channel_message_tests; mod channel_tests; +mod editor_tests; mod following_tests; mod integration_tests; mod notification_tests; diff --git a/crates/collab2/src/tests/channel_buffer_tests.rs b/crates/collab2/src/tests/channel_buffer_tests.rs index ba891e6192b184bf854f7d2b95b1937cf158f672..63057cbd415f011f9269c0f61410afbd12fb0721 100644 --- a/crates/collab2/src/tests/channel_buffer_tests.rs +++ b/crates/collab2/src/tests/channel_buffer_tests.rs @@ -1,288 +1,291 @@ -use crate::{ - rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - tests::TestServer, -}; -use client::{Collaborator, UserId}; -use collections::HashMap; -use futures::future; -use gpui::{BackgroundExecutor, Model, TestAppContext}; -use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; - -#[gpui::test] -async fn test_core_channel_buffers( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) - .await; - - // Client A joins the channel buffer - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - // Client A edits the buffer - let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); - buffer_a.update(cx_a, |buffer, cx| { - buffer.edit([(0..0, "hello world")], None, cx) - }); - buffer_a.update(cx_a, |buffer, cx| { - buffer.edit([(5..5, ", cruel")], None, cx) - }); - buffer_a.update(cx_a, |buffer, cx| { - buffer.edit([(0..5, "goodbye")], None, cx) - }); - buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); - assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); - executor.run_until_parked(); - - // Client B joins the channel buffer - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators( - buffer.collaborators(), - &[client_a.user_id(), client_b.user_id()], - ); - }); - - // Client B sees the correct text, and then edits it - let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); - assert_eq!( - buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), - buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) - ); - assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); - buffer_b.update(cx_b, |buffer, cx| { - buffer.edit([(7..12, "beautiful")], None, cx) - }); - - // Both A and B see the new edit - executor.run_until_parked(); - assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); - assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); - - // Client A closes the channel buffer. - cx_a.update(|_| drop(channel_buffer_a)); - executor.run_until_parked(); - - // Client B sees that client A is gone from the channel buffer. - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); - }); - - // Client A rejoins the channel buffer - let _channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - executor.run_until_parked(); - - // Sanity test, make sure we saw A rejoining - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators( - &buffer.collaborators(), - &[client_a.user_id(), client_b.user_id()], - ); - }); - - // Client A loses connection. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - // Client B observes A disconnect - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); - }); - - // TODO: - // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects - // - Test interaction with channel deletion while buffer is open -} - -// todo!("collab_ui") +//todo(partially ported) +// use std::ops::Range; + +// use crate::{ +// rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, +// tests::TestServer, +// }; +// use client::{Collaborator, ParticipantIndex, UserId}; +// use collections::HashMap; +// use editor::{Anchor, Editor, ToOffset}; +// use futures::future; +// use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; +// use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; + // #[gpui::test] -// async fn test_channel_notes_participant_indices( +// async fn test_core_channel_buffers( // executor: BackgroundExecutor, -// mut cx_a: &mut TestAppContext, -// mut cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// let mut server = TestServer::start(executor.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; - -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); - -// cx_a.update(editor::init); -// cx_b.update(editor::init); -// cx_c.update(editor::init); // let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b), (&client_c, cx_c)], -// ) +// .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) // .await; -// client_a -// .fs() -// .insert_tree("/root", json!({"file.txt": "123"})) -// .await; -// let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; -// let project_b = client_b.build_empty_local_project(cx_b); -// let project_c = client_c.build_empty_local_project(cx_c); -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - -// // Clients A, B, and C open the channel notes -// let channel_view_a = cx_a -// .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) -// .await -// .unwrap(); -// let channel_view_b = cx_b -// .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) -// .await -// .unwrap(); -// let channel_view_c = cx_c -// .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) +// // Client A joins the channel buffer +// let channel_buffer_a = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) // .await // .unwrap(); -// // Clients A, B, and C all insert and select some text -// channel_view_a.update(cx_a, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// editor.insert("a", cx); -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![0..1]); -// }); -// }); +// // Client A edits the buffer +// let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); +// buffer_a.update(cx_a, |buffer, cx| { +// buffer.edit([(0..0, "hello world")], None, cx) // }); -// executor.run_until_parked(); -// channel_view_b.update(cx_b, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// editor.move_down(&Default::default(), cx); -// editor.insert("b", cx); -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![1..2]); -// }); -// }); +// buffer_a.update(cx_a, |buffer, cx| { +// buffer.edit([(5..5, ", cruel")], None, cx) +// }); +// buffer_a.update(cx_a, |buffer, cx| { +// buffer.edit([(0..5, "goodbye")], None, cx) // }); +// buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); +// assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); // executor.run_until_parked(); -// channel_view_c.update(cx_c, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// editor.move_down(&Default::default(), cx); -// editor.insert("c", cx); -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![2..3]); -// }); -// }); + +// // Client B joins the channel buffer +// let channel_buffer_b = client_b +// .channel_store() +// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); +// channel_buffer_b.read_with(cx_b, |buffer, _| { +// assert_collaborators( +// buffer.collaborators(), +// &[client_a.user_id(), client_b.user_id()], +// ); // }); -// // Client A sees clients B and C without assigned colors, because they aren't -// // in a call together. -// executor.run_until_parked(); -// channel_view_a.update(cx_a, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); -// }); +// // Client B sees the correct text, and then edits it +// let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); +// assert_eq!( +// buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), +// buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) +// ); +// assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); +// buffer_b.update(cx_b, |buffer, cx| { +// buffer.edit([(7..12, "beautiful")], None, cx) // }); -// // Clients A and B join the same call. -// for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { -// call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) -// .await -// .unwrap(); -// } +// // Both A and B see the new edit +// executor.run_until_parked(); +// assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); +// assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); -// // Clients A and B see each other with two different assigned colors. Client C -// // still doesn't have a color. +// // Client A closes the channel buffer. +// cx_a.update(|_| drop(channel_buffer_a)); // executor.run_until_parked(); -// channel_view_a.update(cx_a, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// assert_remote_selections( -// editor, -// &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], -// cx, -// ); -// }); -// }); -// channel_view_b.update(cx_b, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// assert_remote_selections( -// editor, -// &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], -// cx, -// ); -// }); -// }); -// // Client A shares a project, and client B joins. -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// // Client B sees that client A is gone from the channel buffer. +// channel_buffer_b.read_with(cx_b, |buffer, _| { +// assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); +// }); -// // Clients A and B open the same file. -// let editor_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) -// }) +// // Client A rejoins the channel buffer +// let _channel_buffer_a = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) // .await -// .unwrap() -// .downcast::() // .unwrap(); - -// editor_a.update(cx_a, |editor, cx| { -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![0..1]); -// }); -// }); -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![2..3]); -// }); -// }); // executor.run_until_parked(); -// // Clients A and B see each other with the same colors as in the channel notes. -// editor_a.update(cx_a, |editor, cx| { -// assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); +// // Sanity test, make sure we saw A rejoining +// channel_buffer_b.read_with(cx_b, |buffer, _| { +// assert_collaborators( +// &buffer.collaborators(), +// &[client_a.user_id(), client_b.user_id()], +// ); // }); -// editor_b.update(cx_b, |editor, cx| { -// assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); + +// // Client A loses connection. +// server.forbid_connections(); +// server.disconnect_client(client_a.peer_id().unwrap()); +// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + +// // Client B observes A disconnect +// channel_buffer_b.read_with(cx_b, |buffer, _| { +// assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); // }); + +// // TODO: +// // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects +// // - Test interaction with channel deletion while buffer is open // } -//todo!(editor) +// // todo!("collab_ui") +// // #[gpui::test] +// // async fn test_channel_notes_participant_indices( +// // executor: BackgroundExecutor, +// // mut cx_a: &mut TestAppContext, +// // mut cx_b: &mut TestAppContext, +// // cx_c: &mut TestAppContext, +// // ) { +// // let mut server = TestServer::start(&executor).await; +// // let client_a = server.create_client(cx_a, "user_a").await; +// // let client_b = server.create_client(cx_b, "user_b").await; +// // let client_c = server.create_client(cx_c, "user_c").await; + +// // let active_call_a = cx_a.read(ActiveCall::global); +// // let active_call_b = cx_b.read(ActiveCall::global); + +// // cx_a.update(editor::init); +// // cx_b.update(editor::init); +// // cx_c.update(editor::init); + +// // let channel_id = server +// // .make_channel( +// // "the-channel", +// // None, +// // (&client_a, cx_a), +// // &mut [(&client_b, cx_b), (&client_c, cx_c)], +// // ) +// // .await; + +// // client_a +// // .fs() +// // .insert_tree("/root", json!({"file.txt": "123"})) +// // .await; +// // let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; +// // let project_b = client_b.build_empty_local_project(cx_b); +// // let project_c = client_c.build_empty_local_project(cx_c); +// // let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// // let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + +// // // Clients A, B, and C open the channel notes +// // let channel_view_a = cx_a +// // .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) +// // .await +// // .unwrap(); +// // let channel_view_b = cx_b +// // .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) +// // .await +// // .unwrap(); +// // let channel_view_c = cx_c +// // .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) +// // .await +// // .unwrap(); + +// // // Clients A, B, and C all insert and select some text +// // channel_view_a.update(cx_a, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // editor.insert("a", cx); +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![0..1]); +// // }); +// // }); +// // }); +// // executor.run_until_parked(); +// // channel_view_b.update(cx_b, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // editor.move_down(&Default::default(), cx); +// // editor.insert("b", cx); +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![1..2]); +// // }); +// // }); +// // }); +// // executor.run_until_parked(); +// // channel_view_c.update(cx_c, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // editor.move_down(&Default::default(), cx); +// // editor.insert("c", cx); +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![2..3]); +// // }); +// // }); +// // }); + +// // // Client A sees clients B and C without assigned colors, because they aren't +// // // in a call together. +// // executor.run_until_parked(); +// // channel_view_a.update(cx_a, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); +// // }); +// // }); + +// // // Clients A and B join the same call. +// // for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { +// // call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) +// // .await +// // .unwrap(); +// // } + +// // // Clients A and B see each other with two different assigned colors. Client C +// // // still doesn't have a color. +// // executor.run_until_parked(); +// // channel_view_a.update(cx_a, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // assert_remote_selections( +// // editor, +// // &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], +// // cx, +// // ); +// // }); +// // }); +// // channel_view_b.update(cx_b, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // assert_remote_selections( +// // editor, +// // &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], +// // cx, +// // ); +// // }); +// // }); + +// // // Client A shares a project, and client B joins. +// // let project_id = active_call_a +// // .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// // .await +// // .unwrap(); +// // let project_b = client_b.build_remote_project(project_id, cx_b).await; +// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + +// // // Clients A and B open the same file. +// // let editor_a = workspace_a +// // .update(cx_a, |workspace, cx| { +// // workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) +// // }) +// // .await +// // .unwrap() +// // .downcast::() +// // .unwrap(); +// // let editor_b = workspace_b +// // .update(cx_b, |workspace, cx| { +// // workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) +// // }) +// // .await +// // .unwrap() +// // .downcast::() +// // .unwrap(); + +// // editor_a.update(cx_a, |editor, cx| { +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![0..1]); +// // }); +// // }); +// // editor_b.update(cx_b, |editor, cx| { +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![2..3]); +// // }); +// // }); +// // executor.run_until_parked(); + +// // // Clients A and B see each other with the same colors as in the channel notes. +// // editor_a.update(cx_a, |editor, cx| { +// // assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); +// // }); +// // editor_b.update(cx_b, |editor, cx| { +// // assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); +// // }); +// // } + // #[track_caller] // fn assert_remote_selections( // editor: &mut Editor, @@ -305,443 +308,135 @@ async fn test_core_channel_buffers( // ); // } -#[gpui::test] -async fn test_multiple_handles_to_channel_buffer( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - - let channel_id = server - .make_channel("the-channel", None, (&client_a, cx_a), &mut []) - .await; - - let channel_buffer_1 = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - let channel_buffer_2 = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - let channel_buffer_3 = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - - // All concurrent tasks for opening a channel buffer return the same model handle. - let (channel_buffer, channel_buffer_2, channel_buffer_3) = - future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) - .await - .unwrap(); - let channel_buffer_model_id = channel_buffer.entity_id(); - assert_eq!(channel_buffer, channel_buffer_2); - assert_eq!(channel_buffer, channel_buffer_3); - - channel_buffer.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "hello")], None, cx); - }) - }); - deterministic.run_until_parked(); - - cx_a.update(|_| { - drop(channel_buffer); - drop(channel_buffer_2); - drop(channel_buffer_3); - }); - deterministic.run_until_parked(); - - // The channel buffer can be reopened after dropping it. - let channel_buffer = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); - channel_buffer.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, _| { - assert_eq!(buffer.text(), "hello"); - }) - }); -} - -#[gpui::test] -async fn test_channel_buffer_disconnect( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b)], - ) - .await; - - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - channel_buffer_a.update(cx_a, |buffer, cx| { - assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); - assert!(!buffer.is_connected()); - }); - - deterministic.run_until_parked(); - - server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - deterministic.run_until_parked(); - - client_a - .channel_store() - .update(cx_a, |channel_store, _| { - channel_store.remove_channel(channel_id) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - - // Channel buffer observed the deletion - channel_buffer_b.update(cx_b, |buffer, cx| { - assert!(buffer.channel(cx).is_none()); - assert!(!buffer.is_connected()); - }); -} - -#[gpui::test] -async fn test_rejoin_channel_buffer( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b)], - ) - .await; - - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - channel_buffer_a.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "1")], None, cx); - }) - }); - deterministic.run_until_parked(); - - // Client A disconnects. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - - // Both clients make an edit. - channel_buffer_a.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(1..1, "2")], None, cx); - }) - }); - channel_buffer_b.update(cx_b, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "0")], None, cx); - }) - }); - - // Both clients see their own edit. - deterministic.run_until_parked(); - channel_buffer_a.read_with(cx_a, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "12"); - }); - channel_buffer_b.read_with(cx_b, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "01"); - }); - - // Client A reconnects. Both clients see each other's edits, and see - // the same collaborators. - server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT); - channel_buffer_a.read_with(cx_a, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - channel_buffer_b.read_with(cx_b, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - - channel_buffer_a.read_with(cx_a, |buffer_a, _| { - channel_buffer_b.read_with(cx_b, |buffer_b, _| { - assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); - }); - }); -} - -#[gpui::test] -async fn test_channel_buffers_and_server_restarts( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b), (&client_c, cx_c)], - ) - .await; - - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let _channel_buffer_c = client_c - .channel_store() - .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - channel_buffer_a.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "1")], None, cx); - }) - }); - deterministic.run_until_parked(); - - // Client C can't reconnect. - client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - - // Server stops. - server.reset().await; - deterministic.advance_clock(RECEIVE_TIMEOUT); - - // While the server is down, both clients make an edit. - channel_buffer_a.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(1..1, "2")], None, cx); - }) - }); - channel_buffer_b.update(cx_b, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "0")], None, cx); - }) - }); - - // Server restarts. - server.start().await.unwrap(); - deterministic.advance_clock(CLEANUP_TIMEOUT); - - // Clients reconnects. Clients A and B see each other's edits, and see - // that client C has disconnected. - channel_buffer_a.read_with(cx_a, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - channel_buffer_b.read_with(cx_b, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - - channel_buffer_a.read_with(cx_a, |buffer_a, _| { - channel_buffer_b.read_with(cx_b, |buffer_b, _| { - assert_collaborators( - buffer_a.collaborators(), - &[client_a.user_id(), client_b.user_id()], - ); - assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); - }); - }); -} - -//todo!(collab_ui) -// #[gpui::test(iterations = 10)] -// async fn test_following_to_channel_notes_without_a_shared_project( +// #[gpui::test] +// async fn test_multiple_handles_to_channel_buffer( // deterministic: BackgroundExecutor, -// mut cx_a: &mut TestAppContext, -// mut cx_b: &mut TestAppContext, -// mut cx_c: &mut TestAppContext, +// cx_a: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&deterministic).await; +// let mut server = TestServer::start(deterministic.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; +// let channel_id = server +// .make_channel("the-channel", None, (&client_a, cx_a), &mut []) +// .await; -// cx_a.update(editor::init); -// cx_b.update(editor::init); -// cx_c.update(editor::init); -// cx_a.update(collab_ui::channel_view::init); -// cx_b.update(collab_ui::channel_view::init); -// cx_c.update(collab_ui::channel_view::init); +// let channel_buffer_1 = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); +// let channel_buffer_2 = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); +// let channel_buffer_3 = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); -// let channel_1_id = server -// .make_channel( -// "channel-1", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b), (&client_c, cx_c)], -// ) -// .await; -// let channel_2_id = server +// // All concurrent tasks for opening a channel buffer return the same model handle. +// let (channel_buffer, channel_buffer_2, channel_buffer_3) = +// future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) +// .await +// .unwrap(); +// let channel_buffer_model_id = channel_buffer.entity_id(); +// assert_eq!(channel_buffer, channel_buffer_2); +// assert_eq!(channel_buffer, channel_buffer_3); + +// channel_buffer.update(cx_a, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, cx| { +// buffer.edit([(0..0, "hello")], None, cx); +// }) +// }); +// deterministic.run_until_parked(); + +// cx_a.update(|_| { +// drop(channel_buffer); +// drop(channel_buffer_2); +// drop(channel_buffer_3); +// }); +// deterministic.run_until_parked(); + +// // The channel buffer can be reopened after dropping it. +// let channel_buffer = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); +// assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); +// channel_buffer.update(cx_a, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, _| { +// assert_eq!(buffer.text(), "hello"); +// }) +// }); +// } + +// #[gpui::test] +// async fn test_channel_buffer_disconnect( +// deterministic: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(deterministic.clone()).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; + +// let channel_id = server // .make_channel( -// "channel-2", +// "the-channel", // None, // (&client_a, cx_a), -// &mut [(&client_b, cx_b), (&client_c, cx_c)], +// &mut [(&client_b, cx_b)], // ) // .await; -// // Clients A, B, and C join a channel. -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); -// let active_call_c = cx_c.read(ActiveCall::global); -// for (call, cx) in [ -// (&active_call_a, &mut cx_a), -// (&active_call_b, &mut cx_b), -// (&active_call_c, &mut cx_c), -// ] { -// call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) -// .await -// .unwrap(); -// } -// deterministic.run_until_parked(); - -// // Clients A, B, and C all open their own unshared projects. -// client_a.fs().insert_tree("/a", json!({})).await; -// client_b.fs().insert_tree("/b", json!({})).await; -// client_c.fs().insert_tree("/c", json!({})).await; -// let (project_a, _) = client_a.build_local_project("/a", cx_a).await; -// let (project_b, _) = client_b.build_local_project("/b", cx_b).await; -// let (project_c, _) = client_b.build_local_project("/c", cx_c).await; -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) +// let channel_buffer_a = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) // .await // .unwrap(); -// // Client A opens the notes for channel 1. -// let channel_view_1_a = cx_a -// .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) +// let channel_buffer_b = client_b +// .channel_store() +// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) // .await // .unwrap(); -// channel_view_1_a.update(cx_a, |notes, cx| { -// assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); -// notes.editor.update(cx, |editor, cx| { -// editor.insert("Hello from A.", cx); -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![3..4]); -// }); -// }); + +// server.forbid_connections(); +// server.disconnect_client(client_a.peer_id().unwrap()); +// deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + +// channel_buffer_a.update(cx_a, |buffer, cx| { +// assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); +// assert!(!buffer.is_connected()); // }); -// // Client B follows client A. -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() -// }) -// .await -// .unwrap(); +// deterministic.run_until_parked(); + +// server.allow_connections(); +// deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); -// // Client B is taken to the notes for channel 1, with the same -// // text selected as client A. // deterministic.run_until_parked(); -// let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.leader_for_pane(workspace.active_pane()), -// Some(client_a.peer_id().unwrap()) -// ); -// workspace -// .active_item(cx) -// .expect("no active item") -// .downcast::() -// .expect("active item is not a channel view") -// }); -// channel_view_1_b.read_with(cx_b, |notes, cx| { -// assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); -// let editor = notes.editor.read(cx); -// assert_eq!(editor.text(cx), "Hello from A."); -// assert_eq!(editor.selections.ranges::(cx), &[3..4]); -// }); -// // Client A opens the notes for channel 2. -// let channel_view_2_a = cx_a -// .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) +// client_a +// .channel_store() +// .update(cx_a, |channel_store, _| { +// channel_store.remove_channel(channel_id) +// }) // .await // .unwrap(); -// channel_view_2_a.read_with(cx_a, |notes, cx| { -// assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); -// }); - -// // Client B is taken to the notes for channel 2. // deterministic.run_until_parked(); -// let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.leader_for_pane(workspace.active_pane()), -// Some(client_a.peer_id().unwrap()) -// ); -// workspace -// .active_item(cx) -// .expect("no active item") -// .downcast::() -// .expect("active item is not a channel view") -// }); -// channel_view_2_b.read_with(cx_b, |notes, cx| { -// assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); + +// // Channel buffer observed the deletion +// channel_buffer_b.update(cx_b, |buffer, cx| { +// assert!(buffer.channel(cx).is_none()); +// assert!(!buffer.is_connected()); // }); // } -//todo!(collab_ui) // #[gpui::test] -// async fn test_channel_buffer_changes( +// async fn test_rejoin_channel_buffer( // deterministic: BackgroundExecutor, // cx_a: &mut TestAppContext, // cx_b: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&deterministic).await; +// let mut server = TestServer::start(deterministic.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; @@ -759,8 +454,12 @@ async fn test_channel_buffers_and_server_restarts( // .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) // .await // .unwrap(); +// let channel_buffer_b = client_b +// .channel_store() +// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); -// // Client A makes an edit, and client B should see that the note has changed. // channel_buffer_a.update(cx_a, |buffer, cx| { // buffer.buffer().update(cx, |buffer, cx| { // buffer.edit([(0..0, "1")], None, cx); @@ -768,105 +467,409 @@ async fn test_channel_buffers_and_server_restarts( // }); // deterministic.run_until_parked(); -// let has_buffer_changed = cx_b.update(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_channel_buffer_changed(channel_id) -// .unwrap() -// }); -// assert!(has_buffer_changed); - -// // Opening the buffer should clear the changed flag. -// let project_b = client_b.build_empty_local_project(cx_b); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let channel_view_b = cx_b -// .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) -// .await -// .unwrap(); -// deterministic.run_until_parked(); - -// let has_buffer_changed = cx_b.update(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_channel_buffer_changed(channel_id) -// .unwrap() -// }); -// assert!(!has_buffer_changed); +// // Client A disconnects. +// server.forbid_connections(); +// server.disconnect_client(client_a.peer_id().unwrap()); -// // Editing the channel while the buffer is open should not show that the buffer has changed. +// // Both clients make an edit. // channel_buffer_a.update(cx_a, |buffer, cx| { // buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "2")], None, cx); +// buffer.edit([(1..1, "2")], None, cx); // }) // }); +// channel_buffer_b.update(cx_b, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, cx| { +// buffer.edit([(0..0, "0")], None, cx); +// }) +// }); + +// // Both clients see their own edit. // deterministic.run_until_parked(); +// channel_buffer_a.read_with(cx_a, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "12"); +// }); +// channel_buffer_b.read_with(cx_b, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "01"); +// }); -// let has_buffer_changed = cx_b.read(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_channel_buffer_changed(channel_id) -// .unwrap() +// // Client A reconnects. Both clients see each other's edits, and see +// // the same collaborators. +// server.allow_connections(); +// deterministic.advance_clock(RECEIVE_TIMEOUT); +// channel_buffer_a.read_with(cx_a, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "012"); // }); -// assert!(!has_buffer_changed); - -// deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); - -// // Test that the server is tracking things correctly, and we retain our 'not changed' -// // state across a disconnect -// server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic); -// let has_buffer_changed = cx_b.read(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_channel_buffer_changed(channel_id) -// .unwrap() +// channel_buffer_b.read_with(cx_b, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "012"); // }); -// assert!(!has_buffer_changed); -// // Closing the buffer should re-enable change tracking -// cx_b.update(|cx| { -// workspace_b.update(cx, |workspace, cx| { -// workspace.close_all_items_and_panes(&Default::default(), cx) +// channel_buffer_a.read_with(cx_a, |buffer_a, _| { +// channel_buffer_b.read_with(cx_b, |buffer_b, _| { +// assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); // }); - -// drop(channel_view_b) // }); +// } -// deterministic.run_until_parked(); +// #[gpui::test] +// async fn test_channel_buffers_and_server_restarts( +// deterministic: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// cx_c: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(deterministic.clone()).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// let client_c = server.create_client(cx_c, "user_c").await; + +// let channel_id = server +// .make_channel( +// "the-channel", +// None, +// (&client_a, cx_a), +// &mut [(&client_b, cx_b), (&client_c, cx_c)], +// ) +// .await; + +// let channel_buffer_a = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); +// let channel_buffer_b = client_b +// .channel_store() +// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); +// let _channel_buffer_c = client_c +// .channel_store() +// .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); // channel_buffer_a.update(cx_a, |buffer, cx| { // buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "3")], None, cx); +// buffer.edit([(0..0, "1")], None, cx); // }) // }); // deterministic.run_until_parked(); -// let has_buffer_changed = cx_b.read(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_channel_buffer_changed(channel_id) -// .unwrap() +// // Client C can't reconnect. +// client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); + +// // Server stops. +// server.reset().await; +// deterministic.advance_clock(RECEIVE_TIMEOUT); + +// // While the server is down, both clients make an edit. +// channel_buffer_a.update(cx_a, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, cx| { +// buffer.edit([(1..1, "2")], None, cx); +// }) +// }); +// channel_buffer_b.update(cx_b, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, cx| { +// buffer.edit([(0..0, "0")], None, cx); +// }) +// }); + +// // Server restarts. +// server.start().await.unwrap(); +// deterministic.advance_clock(CLEANUP_TIMEOUT); + +// // Clients reconnects. Clients A and B see each other's edits, and see +// // that client C has disconnected. +// channel_buffer_a.read_with(cx_a, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "012"); +// }); +// channel_buffer_b.read_with(cx_b, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "012"); +// }); + +// channel_buffer_a.read_with(cx_a, |buffer_a, _| { +// channel_buffer_b.read_with(cx_b, |buffer_b, _| { +// assert_collaborators( +// buffer_a.collaborators(), +// &[client_a.user_id(), client_b.user_id()], +// ); +// assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); +// }); // }); -// assert!(has_buffer_changed); // } -#[track_caller] -fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { - let mut user_ids = collaborators - .values() - .map(|collaborator| collaborator.user_id) - .collect::>(); - user_ids.sort(); - assert_eq!( - user_ids, - ids.into_iter().map(|id| id.unwrap()).collect::>() - ); -} - -fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { - channel_buffer.read_with(cx, |buffer, _| buffer.text()) -} +// //todo!(collab_ui) +// // #[gpui::test(iterations = 10)] +// // async fn test_following_to_channel_notes_without_a_shared_project( +// // deterministic: BackgroundExecutor, +// // mut cx_a: &mut TestAppContext, +// // mut cx_b: &mut TestAppContext, +// // mut cx_c: &mut TestAppContext, +// // ) { +// // let mut server = TestServer::start(&deterministic).await; +// // let client_a = server.create_client(cx_a, "user_a").await; +// // let client_b = server.create_client(cx_b, "user_b").await; + +// // let client_c = server.create_client(cx_c, "user_c").await; + +// // cx_a.update(editor::init); +// // cx_b.update(editor::init); +// // cx_c.update(editor::init); +// // cx_a.update(collab_ui::channel_view::init); +// // cx_b.update(collab_ui::channel_view::init); +// // cx_c.update(collab_ui::channel_view::init); + +// // let channel_1_id = server +// // .make_channel( +// // "channel-1", +// // None, +// // (&client_a, cx_a), +// // &mut [(&client_b, cx_b), (&client_c, cx_c)], +// // ) +// // .await; +// // let channel_2_id = server +// // .make_channel( +// // "channel-2", +// // None, +// // (&client_a, cx_a), +// // &mut [(&client_b, cx_b), (&client_c, cx_c)], +// // ) +// // .await; + +// // // Clients A, B, and C join a channel. +// // let active_call_a = cx_a.read(ActiveCall::global); +// // let active_call_b = cx_b.read(ActiveCall::global); +// // let active_call_c = cx_c.read(ActiveCall::global); +// // for (call, cx) in [ +// // (&active_call_a, &mut cx_a), +// // (&active_call_b, &mut cx_b), +// // (&active_call_c, &mut cx_c), +// // ] { +// // call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) +// // .await +// // .unwrap(); +// // } +// // deterministic.run_until_parked(); + +// // // Clients A, B, and C all open their own unshared projects. +// // client_a.fs().insert_tree("/a", json!({})).await; +// // client_b.fs().insert_tree("/b", json!({})).await; +// // client_c.fs().insert_tree("/c", json!({})).await; +// // let (project_a, _) = client_a.build_local_project("/a", cx_a).await; +// // let (project_b, _) = client_b.build_local_project("/b", cx_b).await; +// // let (project_c, _) = client_b.build_local_project("/c", cx_c).await; +// // let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// // let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + +// // active_call_a +// // .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) +// // .await +// // .unwrap(); + +// // // Client A opens the notes for channel 1. +// // let channel_view_1_a = cx_a +// // .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) +// // .await +// // .unwrap(); +// // channel_view_1_a.update(cx_a, |notes, cx| { +// // assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); +// // notes.editor.update(cx, |editor, cx| { +// // editor.insert("Hello from A.", cx); +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![3..4]); +// // }); +// // }); +// // }); + +// // // Client B follows client A. +// // workspace_b +// // .update(cx_b, |workspace, cx| { +// // workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() +// // }) +// // .await +// // .unwrap(); + +// // // Client B is taken to the notes for channel 1, with the same +// // // text selected as client A. +// // deterministic.run_until_parked(); +// // let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| { +// // assert_eq!( +// // workspace.leader_for_pane(workspace.active_pane()), +// // Some(client_a.peer_id().unwrap()) +// // ); +// // workspace +// // .active_item(cx) +// // .expect("no active item") +// // .downcast::() +// // .expect("active item is not a channel view") +// // }); +// // channel_view_1_b.read_with(cx_b, |notes, cx| { +// // assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); +// // let editor = notes.editor.read(cx); +// // assert_eq!(editor.text(cx), "Hello from A."); +// // assert_eq!(editor.selections.ranges::(cx), &[3..4]); +// // }); + +// // // Client A opens the notes for channel 2. +// // let channel_view_2_a = cx_a +// // .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) +// // .await +// // .unwrap(); +// // channel_view_2_a.read_with(cx_a, |notes, cx| { +// // assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); +// // }); + +// // // Client B is taken to the notes for channel 2. +// // deterministic.run_until_parked(); +// // let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| { +// // assert_eq!( +// // workspace.leader_for_pane(workspace.active_pane()), +// // Some(client_a.peer_id().unwrap()) +// // ); +// // workspace +// // .active_item(cx) +// // .expect("no active item") +// // .downcast::() +// // .expect("active item is not a channel view") +// // }); +// // channel_view_2_b.read_with(cx_b, |notes, cx| { +// // assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); +// // }); +// // } + +// //todo!(collab_ui) +// // #[gpui::test] +// // async fn test_channel_buffer_changes( +// // deterministic: BackgroundExecutor, +// // cx_a: &mut TestAppContext, +// // cx_b: &mut TestAppContext, +// // ) { +// // let mut server = TestServer::start(&deterministic).await; +// // let client_a = server.create_client(cx_a, "user_a").await; +// // let client_b = server.create_client(cx_b, "user_b").await; + +// // let channel_id = server +// // .make_channel( +// // "the-channel", +// // None, +// // (&client_a, cx_a), +// // &mut [(&client_b, cx_b)], +// // ) +// // .await; + +// // let channel_buffer_a = client_a +// // .channel_store() +// // .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) +// // .await +// // .unwrap(); + +// // // Client A makes an edit, and client B should see that the note has changed. +// // channel_buffer_a.update(cx_a, |buffer, cx| { +// // buffer.buffer().update(cx, |buffer, cx| { +// // buffer.edit([(0..0, "1")], None, cx); +// // }) +// // }); +// // deterministic.run_until_parked(); + +// // let has_buffer_changed = cx_b.update(|cx| { +// // client_b +// // .channel_store() +// // .read(cx) +// // .has_channel_buffer_changed(channel_id) +// // .unwrap() +// // }); +// // assert!(has_buffer_changed); + +// // // Opening the buffer should clear the changed flag. +// // let project_b = client_b.build_empty_local_project(cx_b); +// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// // let channel_view_b = cx_b +// // .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) +// // .await +// // .unwrap(); +// // deterministic.run_until_parked(); + +// // let has_buffer_changed = cx_b.update(|cx| { +// // client_b +// // .channel_store() +// // .read(cx) +// // .has_channel_buffer_changed(channel_id) +// // .unwrap() +// // }); +// // assert!(!has_buffer_changed); + +// // // Editing the channel while the buffer is open should not show that the buffer has changed. +// // channel_buffer_a.update(cx_a, |buffer, cx| { +// // buffer.buffer().update(cx, |buffer, cx| { +// // buffer.edit([(0..0, "2")], None, cx); +// // }) +// // }); +// // deterministic.run_until_parked(); + +// // let has_buffer_changed = cx_b.read(|cx| { +// // client_b +// // .channel_store() +// // .read(cx) +// // .has_channel_buffer_changed(channel_id) +// // .unwrap() +// // }); +// // assert!(!has_buffer_changed); + +// // deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); + +// // // Test that the server is tracking things correctly, and we retain our 'not changed' +// // // state across a disconnect +// // server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic); +// // let has_buffer_changed = cx_b.read(|cx| { +// // client_b +// // .channel_store() +// // .read(cx) +// // .has_channel_buffer_changed(channel_id) +// // .unwrap() +// // }); +// // assert!(!has_buffer_changed); + +// // // Closing the buffer should re-enable change tracking +// // cx_b.update(|cx| { +// // workspace_b.update(cx, |workspace, cx| { +// // workspace.close_all_items_and_panes(&Default::default(), cx) +// // }); + +// // drop(channel_view_b) +// // }); + +// // deterministic.run_until_parked(); + +// // channel_buffer_a.update(cx_a, |buffer, cx| { +// // buffer.buffer().update(cx, |buffer, cx| { +// // buffer.edit([(0..0, "3")], None, cx); +// // }) +// // }); +// // deterministic.run_until_parked(); + +// // let has_buffer_changed = cx_b.read(|cx| { +// // client_b +// // .channel_store() +// // .read(cx) +// // .has_channel_buffer_changed(channel_id) +// // .unwrap() +// // }); +// // assert!(has_buffer_changed); +// // } + +// #[track_caller] +// fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { +// let mut user_ids = collaborators +// .values() +// .map(|collaborator| collaborator.user_id) +// .collect::>(); +// user_ids.sort(); +// assert_eq!( +// user_ids, +// ids.into_iter().map(|id| id.unwrap()).collect::>() +// ); +// } + +// fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { +// channel_buffer.read_with(cx, |buffer, _| buffer.text()) +// } diff --git a/crates/collab2/src/tests/editor_tests.rs b/crates/collab2/src/tests/editor_tests.rs index 4900cb20f6c767b0d64a68761a9545ab6d1271de..07a4269567d24481231e5c8bfd1ab155c559bb27 100644 --- a/crates/collab2/src/tests/editor_tests.rs +++ b/crates/collab2/src/tests/editor_tests.rs @@ -1,9 +1,32 @@ +//todo(partially ported) +// use std::{ +// path::Path, +// sync::{ +// atomic::{self, AtomicBool, AtomicUsize}, +// Arc, +// }, +// }; + +// use call::ActiveCall; // use editor::{ -// test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, -// ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo, +// test::editor_test_context::{AssertionContextManager, EditorTestContext}, +// Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, +// ToggleCodeActions, Undo, // }; +// use gpui::{BackgroundExecutor, TestAppContext, VisualContext, VisualTestContext}; +// use indoc::indoc; +// use language::{ +// language_settings::{AllLanguageSettings, InlayHintSettings}, +// tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, +// }; +// use rpc::RECEIVE_TIMEOUT; +// use serde_json::json; +// use settings::SettingsStore; +// use text::Point; +// use workspace::Workspace; + +// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; -//todo!(editor) // #[gpui::test(iterations = 10)] // async fn test_host_disconnect( // executor: BackgroundExecutor, @@ -11,7 +34,7 @@ // cx_b: &mut TestAppContext, // cx_c: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// let mut server = TestServer::start(executor).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; // let client_c = server.create_client(cx_c, "user_c").await; @@ -25,7 +48,7 @@ // .fs() // .insert_tree( // "/a", -// json!({ +// serde_json::json!({ // "a.txt": "a-contents", // "b.txt": "b-contents", // }), @@ -35,7 +58,7 @@ // let active_call_a = cx_a.read(ActiveCall::global); // let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); +// let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees().next().unwrap()); // let project_id = active_call_a // .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) // .await @@ -46,21 +69,25 @@ // assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); -// let window_b = +// let workspace_b = // cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); -// let workspace_b = window_b.root(cx_b); +// let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); + // let editor_b = workspace_b // .update(cx_b, |workspace, cx| { // workspace.open_path((worktree_id, "b.txt"), None, true, cx) // }) +// .unwrap() // .await // .unwrap() // .downcast::() // .unwrap(); -// assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx))); +// //TODO: focus +// assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx))); // editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); -// assert!(window_b.is_edited(cx_b)); +// //todo(is_edited) +// // assert!(workspace_b.is_edited(cx_b)); // // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. // server.forbid_connections(); @@ -77,10 +104,10 @@ // // Ensure client B's edited state is reset and that the whole window is blurred. -// window_b.read_with(cx_b, |cx| { +// workspace_b.update(cx_b, |_, cx| { // assert_eq!(cx.focused_view_id(), None); // }); -// assert!(!window_b.is_edited(cx_b)); +// // assert!(!workspace_b.is_edited(cx_b)); // // Ensure client B is not prompted to save edits when closing window after disconnecting. // let can_close = workspace_b @@ -120,7 +147,6 @@ // project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); // } -//todo!(editor) // #[gpui::test] // async fn test_newline_above_or_below_does_not_move_guest_cursor( // executor: BackgroundExecutor, @@ -152,12 +178,14 @@ // .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) // .await // .unwrap(); -// let window_a = cx_a.add_window(|_| EmptyView); -// let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); +// let window_a = cx_a.add_empty_window(); +// let editor_a = +// window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); // let mut editor_cx_a = EditorTestContext { // cx: cx_a, // window: window_a.into(), // editor: editor_a, +// assertion_cx: AssertionContextManager::new(), // }; // // Open a buffer as client B @@ -165,12 +193,14 @@ // .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) // .await // .unwrap(); -// let window_b = cx_b.add_window(|_| EmptyView); -// let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); +// let window_b = cx_b.add_empty_window(); +// let editor_b = +// window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); // let mut editor_cx_b = EditorTestContext { // cx: cx_b, // window: window_b.into(), // editor: editor_b, +// assertion_cx: AssertionContextManager::new(), // }; // // Test newline above @@ -214,7 +244,6 @@ // "}); // } -//todo!(editor) // #[gpui::test(iterations = 10)] // async fn test_collaborating_with_completion( // executor: BackgroundExecutor, @@ -275,8 +304,8 @@ // .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) // .await // .unwrap(); -// let window_b = cx_b.add_window(|_| EmptyView); -// let editor_b = window_b.add_view(cx_b, |cx| { +// let window_b = cx_b.add_empty_window(); +// let editor_b = window_b.build_view(cx_b, |cx| { // Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) // }); @@ -384,7 +413,7 @@ // ); // // The additional edit is applied. -// cx_a.foreground().run_until_parked(); +// cx_a.executor().run_until_parked(); // buffer_a.read_with(cx_a, |buffer, _| { // assert_eq!( @@ -400,7 +429,7 @@ // ); // }); // } -//todo!(editor) + // #[gpui::test(iterations = 10)] // async fn test_collaborating_with_code_actions( // executor: BackgroundExecutor, @@ -619,7 +648,6 @@ // }); // } -//todo!(editor) // #[gpui::test(iterations = 10)] // async fn test_collaborating_with_renames( // executor: BackgroundExecutor, @@ -813,7 +841,6 @@ // }) // } -//todo!(editor) // #[gpui::test(iterations = 10)] // async fn test_language_server_statuses( // executor: BackgroundExecutor, @@ -937,8 +964,8 @@ // cx_b: &mut TestAppContext, // cx_c: &mut TestAppContext, // ) { -// let window_b = cx_b.add_window(|_| EmptyView); -// let mut server = TestServer::start(&executor).await; +// let window_b = cx_b.add_empty_window(); +// let mut server = TestServer::start(executor).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; // let client_c = server.create_client(cx_c, "user_c").await; @@ -1052,7 +1079,7 @@ // .await // .unwrap(); -// let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); +// let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); // // Client A sees client B's selection // executor.run_until_parked(); @@ -1106,3 +1133,757 @@ // == 0 // }); // } + +// #[gpui::test(iterations = 10)] +// async fn test_on_input_format_from_host_to_guest( +// executor: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(&executor).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// server +// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) +// .await; +// let active_call_a = cx_a.read(ActiveCall::global); + +// // Set up a fake language server. +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_language_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { +// first_trigger_character: ":".to_string(), +// more_trigger_character: Some(vec![">".to_string()]), +// }), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; +// client_a.language_registry().add(Arc::new(language)); + +// client_a +// .fs() +// .insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { a }", +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; +// let project_id = active_call_a +// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// .await +// .unwrap(); +// let project_b = client_b.build_remote_project(project_id, cx_b).await; + +// // Open a file in an editor as the host. +// let buffer_a = project_a +// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) +// .await +// .unwrap(); +// let window_a = cx_a.add_empty_window(); +// let editor_a = window_a +// .update(cx_a, |_, cx| { +// cx.build_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)) +// }) +// .unwrap(); + +// let fake_language_server = fake_language_servers.next().await.unwrap(); +// executor.run_until_parked(); + +// // Receive an OnTypeFormatting request as the host's language server. +// // Return some formattings from the host's language server. +// fake_language_server.handle_request::( +// |params, _| async move { +// assert_eq!( +// params.text_document_position.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// assert_eq!( +// params.text_document_position.position, +// lsp::Position::new(0, 14), +// ); + +// Ok(Some(vec![lsp::TextEdit { +// new_text: "~<".to_string(), +// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), +// }])) +// }, +// ); + +// // Open the buffer on the guest and see that the formattings worked +// let buffer_b = project_b +// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) +// .await +// .unwrap(); + +// // Type a on type formatting trigger character as the guest. +// editor_a.update(cx_a, |editor, cx| { +// cx.focus(&editor_a); +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input(">", cx); +// }); + +// executor.run_until_parked(); + +// buffer_b.read_with(cx_b, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a>~< }") +// }); + +// // Undo should remove LSP edits first +// editor_a.update(cx_a, |editor, cx| { +// assert_eq!(editor.text(cx), "fn main() { a>~< }"); +// editor.undo(&Undo, cx); +// assert_eq!(editor.text(cx), "fn main() { a> }"); +// }); +// executor.run_until_parked(); + +// buffer_b.read_with(cx_b, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a> }") +// }); + +// editor_a.update(cx_a, |editor, cx| { +// assert_eq!(editor.text(cx), "fn main() { a> }"); +// editor.undo(&Undo, cx); +// assert_eq!(editor.text(cx), "fn main() { a }"); +// }); +// executor.run_until_parked(); + +// buffer_b.read_with(cx_b, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a }") +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_on_input_format_from_guest_to_host( +// executor: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(&executor).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// server +// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) +// .await; +// let active_call_a = cx_a.read(ActiveCall::global); + +// // Set up a fake language server. +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_language_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { +// first_trigger_character: ":".to_string(), +// more_trigger_character: Some(vec![">".to_string()]), +// }), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; +// client_a.language_registry().add(Arc::new(language)); + +// client_a +// .fs() +// .insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { a }", +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; +// let project_id = active_call_a +// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// .await +// .unwrap(); +// let project_b = client_b.build_remote_project(project_id, cx_b).await; + +// // Open a file in an editor as the guest. +// let buffer_b = project_b +// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) +// .await +// .unwrap(); +// let window_b = cx_b.add_empty_window(); +// let editor_b = window_b.build_view(cx_b, |cx| { +// Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) +// }); + +// let fake_language_server = fake_language_servers.next().await.unwrap(); +// executor.run_until_parked(); +// // Type a on type formatting trigger character as the guest. +// editor_b.update(cx_b, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input(":", cx); +// cx.focus(&editor_b); +// }); + +// // Receive an OnTypeFormatting request as the host's language server. +// // Return some formattings from the host's language server. +// cx_a.foreground().start_waiting(); +// fake_language_server +// .handle_request::(|params, _| async move { +// assert_eq!( +// params.text_document_position.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// assert_eq!( +// params.text_document_position.position, +// lsp::Position::new(0, 14), +// ); + +// Ok(Some(vec![lsp::TextEdit { +// new_text: "~:".to_string(), +// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), +// }])) +// }) +// .next() +// .await +// .unwrap(); +// cx_a.foreground().finish_waiting(); + +// // Open the buffer on the host and see that the formattings worked +// let buffer_a = project_a +// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) +// .await +// .unwrap(); +// executor.run_until_parked(); + +// buffer_a.read_with(cx_a, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a:~: }") +// }); + +// // Undo should remove LSP edits first +// editor_b.update(cx_b, |editor, cx| { +// assert_eq!(editor.text(cx), "fn main() { a:~: }"); +// editor.undo(&Undo, cx); +// assert_eq!(editor.text(cx), "fn main() { a: }"); +// }); +// executor.run_until_parked(); + +// buffer_a.read_with(cx_a, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a: }") +// }); + +// editor_b.update(cx_b, |editor, cx| { +// assert_eq!(editor.text(cx), "fn main() { a: }"); +// editor.undo(&Undo, cx); +// assert_eq!(editor.text(cx), "fn main() { a }"); +// }); +// executor.run_until_parked(); + +// buffer_a.read_with(cx_a, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a }") +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_mutual_editor_inlay_hint_cache_update( +// executor: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(&executor).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// server +// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) +// .await; +// let active_call_a = cx_a.read(ActiveCall::global); +// let active_call_b = cx_b.read(ActiveCall::global); + +// cx_a.update(editor::init); +// cx_b.update(editor::init); + +// cx_a.update(|cx| { +// cx.update_global(|store: &mut SettingsStore, cx| { +// store.update_user_settings::(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: true, +// show_parameter_hints: false, +// show_other_hints: true, +// }) +// }); +// }); +// }); +// cx_b.update(|cx| { +// cx.update_global(|store: &mut SettingsStore, cx| { +// store.update_user_settings::(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: true, +// show_parameter_hints: false, +// show_other_hints: true, +// }) +// }); +// }); +// }); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_language_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// inlay_hint_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; +// let language = Arc::new(language); +// client_a.language_registry().add(Arc::clone(&language)); +// client_b.language_registry().add(language); + +// // Client A opens a project. +// client_a +// .fs() +// .insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; +// active_call_a +// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) +// .await +// .unwrap(); +// let project_id = active_call_a +// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// .await +// .unwrap(); + +// // Client B joins the project +// let project_b = client_b.build_remote_project(project_id, cx_b).await; +// active_call_b +// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) +// .await +// .unwrap(); + +// let workspace_a = client_a.build_workspace(&project_a, cx_a).root_view(cx_a); +// cx_a.foreground().start_waiting(); + +// // The host opens a rust file. +// let _buffer_a = project_a +// .update(cx_a, |project, cx| { +// project.open_local_buffer("/a/main.rs", cx) +// }) +// .await +// .unwrap(); +// let fake_language_server = fake_language_servers.next().await.unwrap(); +// let editor_a = workspace_a +// .update(cx_a, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// // Set up the language server to return an additional inlay hint on each request. +// let edits_made = Arc::new(AtomicUsize::new(0)); +// let closure_edits_made = Arc::clone(&edits_made); +// fake_language_server +// .handle_request::(move |params, _| { +// let task_edits_made = Arc::clone(&closure_edits_made); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// let edits_made = task_edits_made.load(atomic::Ordering::Acquire); +// Ok(Some(vec![lsp::InlayHint { +// position: lsp::Position::new(0, edits_made as u32), +// label: lsp::InlayHintLabel::String(edits_made.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await +// .unwrap(); + +// executor.run_until_parked(); + +// let initial_edit = edits_made.load(atomic::Ordering::Acquire); +// editor_a.update(cx_a, |editor, _| { +// assert_eq!( +// vec![initial_edit.to_string()], +// extract_hint_labels(editor), +// "Host should get its first hints when opens an editor" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 1, +// "Host editor update the cache version after every cache/view change", +// ); +// }); +// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// let editor_b = workspace_b +// .update(cx_b, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// executor.run_until_parked(); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec![initial_edit.to_string()], +// extract_hint_labels(editor), +// "Client should get its first hints when opens an editor" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 1, +// "Guest editor update the cache version after every cache/view change" +// ); +// }); + +// let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; +// editor_b.update(cx_b, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); +// editor.handle_input(":", cx); +// cx.focus(&editor_b); +// }); + +// executor.run_until_parked(); +// editor_a.update(cx_a, |editor, _| { +// assert_eq!( +// vec![after_client_edit.to_string()], +// extract_hint_labels(editor), +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!(inlay_cache.version(), 2); +// }); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec![after_client_edit.to_string()], +// extract_hint_labels(editor), +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!(inlay_cache.version(), 2); +// }); + +// let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; +// editor_a.update(cx_a, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input("a change to increment both buffers' versions", cx); +// cx.focus(&editor_a); +// }); + +// executor.run_until_parked(); +// editor_a.update(cx_a, |editor, _| { +// assert_eq!( +// vec![after_host_edit.to_string()], +// extract_hint_labels(editor), +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!(inlay_cache.version(), 3); +// }); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec![after_host_edit.to_string()], +// extract_hint_labels(editor), +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!(inlay_cache.version(), 3); +// }); + +// let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; +// fake_language_server +// .request::(()) +// .await +// .expect("inlay refresh request failed"); + +// executor.run_until_parked(); +// editor_a.update(cx_a, |editor, _| { +// assert_eq!( +// vec![after_special_edit_for_refresh.to_string()], +// extract_hint_labels(editor), +// "Host should react to /refresh LSP request" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 4, +// "Host should accepted all edits and bump its cache version every time" +// ); +// }); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec![after_special_edit_for_refresh.to_string()], +// extract_hint_labels(editor), +// "Guest should get a /refresh LSP request propagated by host" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 4, +// "Guest should accepted all edits and bump its cache version every time" +// ); +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_inlay_hint_refresh_is_forwarded( +// executor: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(&executor).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// server +// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) +// .await; +// let active_call_a = cx_a.read(ActiveCall::global); +// let active_call_b = cx_b.read(ActiveCall::global); + +// cx_a.update(editor::init); +// cx_b.update(editor::init); + +// cx_a.update(|cx| { +// cx.update_global(|store: &mut SettingsStore, cx| { +// store.update_user_settings::(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: false, +// show_type_hints: false, +// show_parameter_hints: false, +// show_other_hints: false, +// }) +// }); +// }); +// }); +// cx_b.update(|cx| { +// cx.update_global(|store: &mut SettingsStore, cx| { +// store.update_user_settings::(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: true, +// show_parameter_hints: true, +// show_other_hints: true, +// }) +// }); +// }); +// }); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_language_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// inlay_hint_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; +// let language = Arc::new(language); +// client_a.language_registry().add(Arc::clone(&language)); +// client_b.language_registry().add(language); + +// client_a +// .fs() +// .insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; +// active_call_a +// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) +// .await +// .unwrap(); +// let project_id = active_call_a +// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// .await +// .unwrap(); + +// let project_b = client_b.build_remote_project(project_id, cx_b).await; +// active_call_b +// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) +// .await +// .unwrap(); + +// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// cx_a.foreground().start_waiting(); +// cx_b.foreground().start_waiting(); + +// let editor_a = workspace_a +// .update(cx_a, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// let editor_b = workspace_b +// .update(cx_b, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// let other_hints = Arc::new(AtomicBool::new(false)); +// let fake_language_server = fake_language_servers.next().await.unwrap(); +// let closure_other_hints = Arc::clone(&other_hints); +// fake_language_server +// .handle_request::(move |params, _| { +// let task_other_hints = Arc::clone(&closure_other_hints); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// let other_hints = task_other_hints.load(atomic::Ordering::Acquire); +// let character = if other_hints { 0 } else { 2 }; +// let label = if other_hints { +// "other hint" +// } else { +// "initial hint" +// }; +// Ok(Some(vec![lsp::InlayHint { +// position: lsp::Position::new(0, character), +// label: lsp::InlayHintLabel::String(label.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await +// .unwrap(); +// cx_a.foreground().finish_waiting(); +// cx_b.foreground().finish_waiting(); + +// executor.run_until_parked(); +// editor_a.update(cx_a, |editor, _| { +// assert!( +// extract_hint_labels(editor).is_empty(), +// "Host should get no hints due to them turned off" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 0, +// "Turned off hints should not generate version updates" +// ); +// }); + +// executor.run_until_parked(); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec!["initial hint".to_string()], +// extract_hint_labels(editor), +// "Client should get its first hints when opens an editor" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 1, +// "Should update cache verison after first hints" +// ); +// }); + +// other_hints.fetch_or(true, atomic::Ordering::Release); +// fake_language_server +// .request::(()) +// .await +// .expect("inlay refresh request failed"); +// executor.run_until_parked(); +// editor_a.update(cx_a, |editor, _| { +// assert!( +// extract_hint_labels(editor).is_empty(), +// "Host should get nop hints due to them turned off, even after the /refresh" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 0, +// "Turned off hints should not generate version updates, again" +// ); +// }); + +// executor.run_until_parked(); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec!["other hint".to_string()], +// extract_hint_labels(editor), +// "Guest should get a /refresh LSP request propagated by host despite host hints are off" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 2, +// "Guest should accepted all edits and bump its cache version every time" +// ); +// }); +// } + +// fn extract_hint_labels(editor: &Editor) -> Vec { +// let mut labels = Vec::new(); +// for hint in editor.inlay_hint_cache().hints() { +// match hint.label { +// project::InlayHintLabel::String(s) => labels.push(s), +// _ => unreachable!(), +// } +// } +// labels +// } diff --git a/crates/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs index f681e4877fa25bd97c980a5a7003f981ad417682..121a98c1d2ce766bb2a5a3d7dcce5d31a007ebf8 100644 --- a/crates/collab2/src/tests/integration_tests.rs +++ b/crates/collab2/src/tests/integration_tests.rs @@ -5717,758 +5717,3 @@ async fn test_join_call_after_screen_was_shared( ); }); } - -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_on_input_format_from_host_to_guest( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { -// first_trigger_character: ":".to_string(), -// more_trigger_character: Some(vec![">".to_string()]), -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; - -// // Open a file in an editor as the host. -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// let window_a = cx_a.add_window(|_| EmptyView); -// let editor_a = window_a.add_view(cx_a, |cx| { -// Editor::for_buffer(buffer_a, Some(project_a.clone()), cx) -// }); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// executor.run_until_parked(); - -// // Receive an OnTypeFormatting request as the host's language server. -// // Return some formattings from the host's language server. -// fake_language_server.handle_request::( -// |params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 14), -// ); - -// Ok(Some(vec![lsp::TextEdit { -// new_text: "~<".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// }])) -// }, -// ); - -// // Open the buffer on the guest and see that the formattings worked -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); - -// // Type a on type formatting trigger character as the guest. -// editor_a.update(cx_a, |editor, cx| { -// cx.focus(&editor_a); -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(">", cx); -// }); - -// executor.run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a>~< }") -// }); - -// // Undo should remove LSP edits first -// editor_a.update(cx_a, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a>~< }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a> }"); -// }); -// executor.run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a> }") -// }); - -// editor_a.update(cx_a, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a> }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a }"); -// }); -// executor.run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a }") -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_on_input_format_from_guest_to_host( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { -// first_trigger_character: ":".to_string(), -// more_trigger_character: Some(vec![">".to_string()]), -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; - -// // Open a file in an editor as the guest. -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// let window_b = cx_b.add_window(|_| EmptyView); -// let editor_b = window_b.add_view(cx_b, |cx| { -// Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) -// }); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// executor.run_until_parked(); -// // Type a on type formatting trigger character as the guest. -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(":", cx); -// cx.focus(&editor_b); -// }); - -// // Receive an OnTypeFormatting request as the host's language server. -// // Return some formattings from the host's language server. -// cx_a.foreground().start_waiting(); -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 14), -// ); - -// Ok(Some(vec![lsp::TextEdit { -// new_text: "~:".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// }])) -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); - -// // Open the buffer on the host and see that the formattings worked -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a:~: }") -// }); - -// // Undo should remove LSP edits first -// editor_b.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a:~: }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a: }"); -// }); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a: }") -// }); - -// editor_b.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a: }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a }"); -// }); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a }") -// }); -// } - -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_mutual_editor_inlay_hint_cache_update( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); - -// cx_a.update(editor::init); -// cx_b.update(editor::init); - -// cx_a.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: false, -// show_other_hints: true, -// }) -// }); -// }); -// }); -// cx_b.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: false, -// show_other_hints: true, -// }) -// }); -// }); -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// client_a.language_registry().add(Arc::clone(&language)); -// client_b.language_registry().add(language); - -// // Client A opens a project. -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// // Client B joins the project -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// cx_a.foreground().start_waiting(); - -// // The host opens a rust file. -// let _buffer_a = project_a -// .update(cx_a, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// let editor_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Set up the language server to return an additional inlay hint on each request. -// let edits_made = Arc::new(AtomicUsize::new(0)); -// let closure_edits_made = Arc::clone(&edits_made); -// fake_language_server -// .handle_request::(move |params, _| { -// let task_edits_made = Arc::clone(&closure_edits_made); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let edits_made = task_edits_made.load(atomic::Ordering::Acquire); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, edits_made as u32), -// label: lsp::InlayHintLabel::String(edits_made.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await -// .unwrap(); - -// executor.run_until_parked(); - -// let initial_edit = edits_made.load(atomic::Ordering::Acquire); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![initial_edit.to_string()], -// extract_hint_labels(editor), -// "Host should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Host editor update the cache version after every cache/view change", -// ); -// }); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![initial_edit.to_string()], -// extract_hint_labels(editor), -// "Client should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Guest editor update the cache version after every cache/view change" -// ); -// }); - -// let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); -// editor.handle_input(":", cx); -// cx.focus(&editor_b); -// }); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_client_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 2); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_client_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 2); -// }); - -// let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// editor_a.update(cx_a, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("a change to increment both buffers' versions", cx); -// cx.focus(&editor_a); -// }); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_host_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 3); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_host_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 3); -// }); - -// let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// fake_language_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_special_edit_for_refresh.to_string()], -// extract_hint_labels(editor), -// "Host should react to /refresh LSP request" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 4, -// "Host should accepted all edits and bump its cache version every time" -// ); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_special_edit_for_refresh.to_string()], -// extract_hint_labels(editor), -// "Guest should get a /refresh LSP request propagated by host" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 4, -// "Guest should accepted all edits and bump its cache version every time" -// ); -// }); -// } - -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_inlay_hint_refresh_is_forwarded( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); - -// cx_a.update(editor::init); -// cx_b.update(editor::init); - -// cx_a.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: false, -// show_type_hints: false, -// show_parameter_hints: false, -// show_other_hints: false, -// }) -// }); -// }); -// }); -// cx_b.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); -// }); -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// client_a.language_registry().add(Arc::clone(&language)); -// client_b.language_registry().add(language); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// cx_a.foreground().start_waiting(); -// cx_b.foreground().start_waiting(); - -// let editor_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let other_hints = Arc::new(AtomicBool::new(false)); -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// let closure_other_hints = Arc::clone(&other_hints); -// fake_language_server -// .handle_request::(move |params, _| { -// let task_other_hints = Arc::clone(&closure_other_hints); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let other_hints = task_other_hints.load(atomic::Ordering::Acquire); -// let character = if other_hints { 0 } else { 2 }; -// let label = if other_hints { -// "other hint" -// } else { -// "initial hint" -// }; -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, character), -// label: lsp::InlayHintLabel::String(label.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); -// cx_b.foreground().finish_waiting(); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert!( -// extract_hint_labels(editor).is_empty(), -// "Host should get no hints due to them turned off" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 0, -// "Turned off hints should not generate version updates" -// ); -// }); - -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec!["initial hint".to_string()], -// extract_hint_labels(editor), -// "Client should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Should update cache verison after first hints" -// ); -// }); - -// other_hints.fetch_or(true, atomic::Ordering::Release); -// fake_language_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert!( -// extract_hint_labels(editor).is_empty(), -// "Host should get nop hints due to them turned off, even after the /refresh" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 0, -// "Turned off hints should not generate version updates, again" -// ); -// }); - -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec!["other hint".to_string()], -// extract_hint_labels(editor), -// "Guest should get a /refresh LSP request propagated by host despite host hints are off" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 2, -// "Guest should accepted all edits and bump its cache version every time" -// ); -// }); -// } - -// fn extract_hint_labels(editor: &Editor) -> Vec { -// let mut labels = Vec::new(); -// for hint in editor.inlay_hint_cache().hints() { -// match hint.label { -// project::InlayHintLabel::String(s) => labels.push(s), -// _ => unreachable!(), -// } -// } -// labels -// } diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index d0ab917d68ec7f84090fdfb213839fcc2e800f64..1b4d8945ae60d9acf3d8d90fab77ff728a1e8f2c 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -208,11 +208,11 @@ impl TestServer { }) }); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx)); let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); let mut language_registry = LanguageRegistry::test(); - language_registry.set_executor(cx.executor().clone()); + language_registry.set_executor(cx.executor()); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index cb0f2c20d41f5f48b016f5cf74395343e9e4e0b6..435a6446693e590f2af9c678df445c0f53b18428 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -11,7 +11,7 @@ use std::{ sync::Arc, }; use theme::ActiveTheme; -use ui::{v_stack, HighlightedLabel, StyledExt}; +use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -130,16 +130,7 @@ impl CommandPaletteDelegate { ) -> Self { Self { command_palette, - matches: commands - .iter() - .enumerate() - .map(|(i, command)| StringMatch { - candidate_id: i, - string: command.name.clone(), - positions: Vec::new(), - score: 0.0, - }) - .collect(), + matches: vec![], commands, selected_ix: 0, previous_focus_handle, @@ -318,66 +309,16 @@ impl PickerDelegate for CommandPaletteDelegate { .rounded_md() .when(selected, |this| this.bg(colors.ghost_element_selected)) .hover(|this| this.bg(colors.ghost_element_hover)) - .child(HighlightedLabel::new( - command.name.clone(), - r#match.positions.clone(), - )) + .child( + h_stack() + .justify_between() + .child(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) + .children(KeyBinding::for_action(&*command.action, cx)), + ) } - - // fn render_match( - // &self, - // ix: usize, - // mouse_state: &mut MouseState, - // selected: bool, - // cx: &gpui::AppContext, - // ) -> AnyElement> { - // let mat = &self.matches[ix]; - // let command = &self.actions[mat.candidate_id]; - // let theme = theme::current(cx); - // let style = theme.picker.item.in_state(selected).style_for(mouse_state); - // let key_style = &theme.command_palette.key.in_state(selected); - // let keystroke_spacing = theme.command_palette.keystroke_spacing; - - // Flex::row() - // .with_child( - // Label::new(mat.string.clone(), style.label.clone()) - // .with_highlights(mat.positions.clone()), - // ) - // .with_children(command.keystrokes.iter().map(|keystroke| { - // Flex::row() - // .with_children( - // [ - // (keystroke.ctrl, "^"), - // (keystroke.alt, "⌥"), - // (keystroke.cmd, "⌘"), - // (keystroke.shift, "⇧"), - // ] - // .into_iter() - // .filter_map(|(modifier, label)| { - // if modifier { - // Some( - // Label::new(label, key_style.label.clone()) - // .contained() - // .with_style(key_style.container), - // ) - // } else { - // None - // } - // }), - // ) - // .with_child( - // Label::new(keystroke.key.clone(), key_style.label.clone()) - // .contained() - // .with_style(key_style.container), - // ) - // .contained() - // .with_margin_left(keystroke_spacing) - // .flex_float() - // })) - // .contained() - // .with_style(style.container) - // .into_any() - // } } fn humanize_action_name(name: &str) -> String { diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml index 2ce432d9fc2ff877e5982aa7d2a3d83e61cc9491..68b56a6c018b5108c841c37e83f68c0877ee77f5 100644 --- a/crates/copilot2/Cargo.toml +++ b/crates/copilot2/Cargo.toml @@ -24,7 +24,7 @@ collections = { path = "../collections" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } settings = { package = "settings2", path = "../settings2" } -theme = { path = "../theme" } +theme = { package = "theme2", path = "../theme2" } lsp = { package = "lsp2", path = "../lsp2" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index 2daf2fec12434f23071135741c6a07a5dd5f847c..53d802dd037f51c3df4183ba57bda44a1ea18e7f 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -351,28 +351,29 @@ impl Copilot { } } - // #[cfg(any(test, feature = "test-support"))] - // pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle, lsp::FakeLanguageServer) { - // use node_runtime::FakeNodeRuntime; - - // let (server, fake_server) = - // LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); - // let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); - // let node_runtime = FakeNodeRuntime::new(); - // let this = cx.add_model(|_| Self { - // server_id: LanguageServerId(0), - // http: http.clone(), - // node_runtime, - // server: CopilotServer::Running(RunningCopilotServer { - // name: LanguageServerName(Arc::from("copilot")), - // lsp: Arc::new(server), - // sign_in_status: SignInStatus::Authorized, - // registered_buffers: Default::default(), - // }), - // buffers: Default::default(), - // }); - // (this, fake_server) - // } + #[cfg(any(test, feature = "test-support"))] + pub fn fake(cx: &mut gpui::TestAppContext) -> (Model, lsp::FakeLanguageServer) { + use node_runtime::FakeNodeRuntime; + + let (server, fake_server) = + LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); + let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); + let node_runtime = FakeNodeRuntime::new(); + let this = cx.build_model(|cx| Self { + server_id: LanguageServerId(0), + http: http.clone(), + node_runtime, + server: CopilotServer::Running(RunningCopilotServer { + name: LanguageServerName(Arc::from("copilot")), + lsp: Arc::new(server), + sign_in_status: SignInStatus::Authorized, + registered_buffers: Default::default(), + }), + _subscription: cx.on_app_quit(Self::shutdown_language_server), + buffers: Default::default(), + }); + (this, fake_server) + } fn start_language_server( new_server_id: LanguageServerId, diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml index e45c33d91759f2e7d21283e45d1011cdd26abcb8..98bd06bc4d80ea7c7624bcc8596325749877464f 100644 --- a/crates/editor2/Cargo.toml +++ b/crates/editor2/Cargo.toml @@ -27,7 +27,6 @@ client = { package = "client2", path = "../client2" } clock = { path = "../clock" } copilot = { package="copilot2", path = "../copilot2" } db = { package="db2", path = "../db2" } -drag_and_drop = { path = "../drag_and_drop" } collections = { path = "../collections" } # context_menu = { path = "../context_menu" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index f4e4146f1a094c2dca6e9f25b5a0a04f0240c4d5..33c4dd63901855929dce360b52187c0c72f93501 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -2023,24 +2023,24 @@ impl Editor { dispatch_context } - // pub fn new_file( - // workspace: &mut Workspace, - // _: &workspace::NewFile, - // cx: &mut ViewContext, - // ) { - // let project = workspace.project().clone(); - // if project.read(cx).is_remote() { - // cx.propagate(); - // } else if let Some(buffer) = project - // .update(cx, |project, cx| project.create_buffer("", None, cx)) - // .log_err() - // { - // workspace.add_item( - // Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), - // cx, - // ); - // } - // } + pub fn new_file( + workspace: &mut Workspace, + _: &workspace::NewFile, + cx: &mut ViewContext, + ) { + let project = workspace.project().clone(); + if project.read(cx).is_remote() { + cx.propagate(); + } else if let Some(buffer) = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .log_err() + { + workspace.add_item( + Box::new(cx.build_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), + cx, + ); + } + } // pub fn new_file_in_direction( // workspace: &mut Workspace, @@ -2124,17 +2124,17 @@ impl Editor { // ) // } - // pub fn mode(&self) -> EditorMode { - // self.mode - // } + pub fn mode(&self) -> EditorMode { + self.mode + } - // pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { - // self.collaboration_hub.as_deref() - // } + pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { + self.collaboration_hub.as_deref() + } - // pub fn set_collaboration_hub(&mut self, hub: Box) { - // self.collaboration_hub = Some(hub); - // } + pub fn set_collaboration_hub(&mut self, hub: Box) { + self.collaboration_hub = Some(hub); + } pub fn set_placeholder_text( &mut self, @@ -9383,7 +9383,7 @@ impl Render for Editor { color: cx.theme().colors().text, font_family: "Zed Sans".into(), // todo!() font_features: FontFeatures::default(), - font_size: rems(1.0).into(), + font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()), @@ -10056,76 +10056,76 @@ pub fn diagnostic_style( } } -// pub fn combine_syntax_and_fuzzy_match_highlights( -// text: &str, -// default_style: HighlightStyle, -// syntax_ranges: impl Iterator, HighlightStyle)>, -// match_indices: &[usize], -// ) -> Vec<(Range, HighlightStyle)> { -// let mut result = Vec::new(); -// let mut match_indices = match_indices.iter().copied().peekable(); - -// for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) -// { -// syntax_highlight.weight = None; - -// // Add highlights for any fuzzy match characters before the next -// // syntax highlight range. -// while let Some(&match_index) = match_indices.peek() { -// if match_index >= range.start { -// break; -// } -// match_indices.next(); -// let end_index = char_ix_after(match_index, text); -// let mut match_style = default_style; -// match_style.weight = Some(FontWeight::BOLD); -// result.push((match_index..end_index, match_style)); -// } +pub fn combine_syntax_and_fuzzy_match_highlights( + text: &str, + default_style: HighlightStyle, + syntax_ranges: impl Iterator, HighlightStyle)>, + match_indices: &[usize], +) -> Vec<(Range, HighlightStyle)> { + let mut result = Vec::new(); + let mut match_indices = match_indices.iter().copied().peekable(); -// if range.start == usize::MAX { -// break; -// } + for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) + { + syntax_highlight.font_weight = None; -// // Add highlights for any fuzzy match characters within the -// // syntax highlight range. -// let mut offset = range.start; -// while let Some(&match_index) = match_indices.peek() { -// if match_index >= range.end { -// break; -// } + // Add highlights for any fuzzy match characters before the next + // syntax highlight range. + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.start { + break; + } + match_indices.next(); + let end_index = char_ix_after(match_index, text); + let mut match_style = default_style; + match_style.font_weight = Some(FontWeight::BOLD); + result.push((match_index..end_index, match_style)); + } -// match_indices.next(); -// if match_index > offset { -// result.push((offset..match_index, syntax_highlight)); -// } + if range.start == usize::MAX { + break; + } -// let mut end_index = char_ix_after(match_index, text); -// while let Some(&next_match_index) = match_indices.peek() { -// if next_match_index == end_index && next_match_index < range.end { -// end_index = char_ix_after(next_match_index, text); -// match_indices.next(); -// } else { -// break; -// } -// } + // Add highlights for any fuzzy match characters within the + // syntax highlight range. + let mut offset = range.start; + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.end { + break; + } -// let mut match_style = syntax_highlight; -// match_style.weight = Some(FontWeight::BOLD); -// result.push((match_index..end_index, match_style)); -// offset = end_index; -// } + match_indices.next(); + if match_index > offset { + result.push((offset..match_index, syntax_highlight)); + } -// if offset < range.end { -// result.push((offset..range.end, syntax_highlight)); -// } -// } + let mut end_index = char_ix_after(match_index, text); + while let Some(&next_match_index) = match_indices.peek() { + if next_match_index == end_index && next_match_index < range.end { + end_index = char_ix_after(next_match_index, text); + match_indices.next(); + } else { + break; + } + } -// fn char_ix_after(ix: usize, text: &str) -> usize { -// ix + text[ix..].chars().next().unwrap().len_utf8() -// } + let mut match_style = syntax_highlight; + match_style.font_weight = Some(FontWeight::BOLD); + result.push((match_index..end_index, match_style)); + offset = end_index; + } -// result -// } + if offset < range.end { + result.push((offset..range.end, syntax_highlight)); + } + } + + fn char_ix_after(ix: usize, text: &str) -> usize { + ix + text[ix..].chars().next().unwrap().len_utf8() + } + + result +} // pub fn styled_runs_for_code_label<'a>( // label: &'a CodeLabel, diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 0ba015804529c1e778408c058e0b4686ecbefbfb..9798735bf68a224411713129dc9ccb8428c5cc7f 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -1,92 +1,80 @@ -use gpui::TestAppContext; -use language::language_settings::{AllLanguageSettings, AllLanguageSettingsContent}; -use settings::SettingsStore; - -// use super::*; -// use crate::{ -// scroll::scroll_amount::ScrollAmount, -// test::{ -// assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, -// editor_test_context::EditorTestContext, select_ranges, -// }, -// JoinLines, -// }; -// use drag_and_drop::DragAndDrop; -// use futures::StreamExt; -// use gpui::{ -// executor::Deterministic, -// geometry::{rect::RectF, vector::vec2f}, -// platform::{WindowBounds, WindowOptions}, -// serde_json::{self, json}, -// TestAppContext, -// }; -// use indoc::indoc; -// use language::{ -// language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, -// BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, -// Override, Point, -// }; -// use parking_lot::Mutex; -// use project::project_settings::{LspSettings, ProjectSettings}; -// use project::FakeFs; -// use std::sync::atomic; -// use std::sync::atomic::AtomicUsize; -// use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; -// use unindent::Unindent; -// use util::{ -// assert_set_eq, -// test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, -// }; -// use workspace::{ -// item::{FollowableItem, Item, ItemHandle}, -// NavigationEntry, ViewId, -// }; - +use super::*; +use crate::{ + scroll::scroll_amount::ScrollAmount, + test::{ + assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, + editor_test_context::EditorTestContext, select_ranges, + }, + JoinLines, +}; + +use futures::StreamExt; +use gpui::{ + div, + serde_json::{self, json}, + Div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions, +}; +use indoc::indoc; +use language::{ + language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, + BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, + Override, Point, +}; +use parking_lot::Mutex; +use project::project_settings::{LspSettings, ProjectSettings}; +use project::FakeFs; +use std::sync::atomic; +use std::sync::atomic::AtomicUsize; +use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; +use unindent::Unindent; +use util::{ + assert_set_eq, + test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, +}; +use workspace::{ + item::{FollowEvent, FollowableEvents, FollowableItem, Item, ItemHandle}, + NavigationEntry, ViewId, +}; + +// todo(finish edit tests) // #[gpui::test] // fn test_edit_events(cx: &mut TestAppContext) { // init_test(cx, |_| {}); -// let buffer = cx.add_model(|cx| { -// let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456"); +// 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| { -// cx.subscribe(&cx.handle(), move |_, _, event, _| { -// if matches!( -// event, -// Event::Edited | Event::BufferEdited | Event::DirtyChanged -// ) { -// events.borrow_mut().push(("editor1", event.clone())); -// } -// }) -// .detach(); -// Editor::for_buffer(buffer.clone(), None, cx) -// } -// }) -// .root(cx); -// let editor2 = cx -// .add_window({ -// let events = events.clone(); -// |cx| { -// cx.subscribe(&cx.handle(), move |_, _, event, _| { -// if matches!( -// event, -// Event::Edited | Event::BufferEdited | Event::DirtyChanged -// ) { -// events.borrow_mut().push(("editor2", event.clone())); -// } -// }) -// .detach(); -// Editor::for_buffer(buffer.clone(), None, cx) -// } -// }) -// .root(cx); +// 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. @@ -97,8 +85,6 @@ use settings::SettingsStore; // ("editor1", Event::Edited), // ("editor1", Event::BufferEdited), // ("editor2", Event::BufferEdited), -// ("editor1", Event::DirtyChanged), -// ("editor2", Event::DirtyChanged) // ] // ); @@ -121,8 +107,6 @@ use settings::SettingsStore; // ("editor1", Event::Edited), // ("editor1", Event::BufferEdited), // ("editor2", Event::BufferEdited), -// ("editor1", Event::DirtyChanged), -// ("editor2", Event::DirtyChanged), // ] // ); @@ -134,8 +118,6 @@ use settings::SettingsStore; // ("editor1", Event::Edited), // ("editor1", Event::BufferEdited), // ("editor2", Event::BufferEdited), -// ("editor1", Event::DirtyChanged), -// ("editor2", Event::DirtyChanged), // ] // ); @@ -147,8 +129,6 @@ use settings::SettingsStore; // ("editor2", Event::Edited), // ("editor1", Event::BufferEdited), // ("editor2", Event::BufferEdited), -// ("editor1", Event::DirtyChanged), -// ("editor2", Event::DirtyChanged), // ] // ); @@ -160,8 +140,6 @@ use settings::SettingsStore; // ("editor2", Event::Edited), // ("editor1", Event::BufferEdited), // ("editor2", Event::BufferEdited), -// ("editor1", Event::DirtyChanged), -// ("editor2", Event::DirtyChanged), // ] // ); @@ -174,6095 +152,6198 @@ use settings::SettingsStore; // assert_eq!(mem::take(&mut *events.borrow_mut()), []); // } -// #[gpui::test] -// fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let mut now = Instant::now(); -// let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456")); -// let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx -// .add_window(|cx| build_editor(buffer.clone(), cx)) -// .root(cx); - -// editor.update(cx, |editor, cx| { -// editor.start_transaction_at(now, cx); -// editor.change_selections(None, cx, |s| s.select_ranges([2..4])); - -// editor.insert("cd", cx); -// editor.end_transaction_at(now, cx); -// assert_eq!(editor.text(cx), "12cd56"); -// assert_eq!(editor.selections.ranges(cx), vec![4..4]); - -// editor.start_transaction_at(now, cx); -// editor.change_selections(None, cx, |s| s.select_ranges([4..5])); -// editor.insert("e", cx); -// editor.end_transaction_at(now, cx); -// assert_eq!(editor.text(cx), "12cde6"); -// assert_eq!(editor.selections.ranges(cx), vec![5..5]); - -// now += group_interval + Duration::from_millis(1); -// editor.change_selections(None, cx, |s| s.select_ranges([2..2])); - -// // Simulate an edit in another editor -// buffer.update(cx, |buffer, cx| { -// buffer.start_transaction_at(now, cx); -// buffer.edit([(0..1, "a")], None, cx); -// buffer.edit([(1..1, "b")], None, cx); -// buffer.end_transaction_at(now, cx); -// }); +#[gpui::test] +fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut now = Instant::now(); + let buffer = cx.build_model(|cx| language::Buffer::new(0, cx.entity_id().as_u64(), "123456")); + let group_interval = buffer.update(cx, |buffer, _| buffer.transaction_group_interval()); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx)); + + editor.update(cx, |editor, cx| { + editor.start_transaction_at(now, cx); + editor.change_selections(None, cx, |s| s.select_ranges([2..4])); + + editor.insert("cd", cx); + editor.end_transaction_at(now, cx); + assert_eq!(editor.text(cx), "12cd56"); + assert_eq!(editor.selections.ranges(cx), vec![4..4]); + + editor.start_transaction_at(now, cx); + editor.change_selections(None, cx, |s| s.select_ranges([4..5])); + editor.insert("e", cx); + editor.end_transaction_at(now, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selections.ranges(cx), vec![5..5]); + + now += group_interval + Duration::from_millis(1); + editor.change_selections(None, cx, |s| s.select_ranges([2..2])); + + // Simulate an edit in another editor + buffer.update(cx, |buffer, cx| { + buffer.start_transaction_at(now, cx); + buffer.edit([(0..1, "a")], None, cx); + buffer.edit([(1..1, "b")], None, cx); + buffer.end_transaction_at(now, cx); + }); -// assert_eq!(editor.text(cx), "ab2cde6"); -// assert_eq!(editor.selections.ranges(cx), vec![3..3]); + assert_eq!(editor.text(cx), "ab2cde6"); + assert_eq!(editor.selections.ranges(cx), vec![3..3]); + + // Last transaction happened past the group interval in a different editor. + // Undo it individually and don't restore selections. + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selections.ranges(cx), vec![2..2]); + + // First two transactions happened within the group interval in this editor. + // Undo them together and restore selections. + editor.undo(&Undo, cx); + editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. + assert_eq!(editor.text(cx), "123456"); + assert_eq!(editor.selections.ranges(cx), vec![0..0]); + + // Redo the first two transactions together. + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selections.ranges(cx), vec![5..5]); + + // Redo the last transaction on its own. + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "ab2cde6"); + assert_eq!(editor.selections.ranges(cx), vec![6..6]); + + // Test empty transactions. + editor.start_transaction_at(now, cx); + editor.end_transaction_at(now, cx); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "12cde6"); + }); +} -// // Last transaction happened past the group interval in a different editor. -// // Undo it individually and don't restore selections. -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "12cde6"); -// assert_eq!(editor.selections.ranges(cx), vec![2..2]); +#[gpui::test] +fn test_ime_composition(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// // First two transactions happened within the group interval in this editor. -// // Undo them together and restore selections. -// editor.undo(&Undo, cx); -// editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. -// assert_eq!(editor.text(cx), "123456"); -// assert_eq!(editor.selections.ranges(cx), vec![0..0]); - -// // Redo the first two transactions together. -// editor.redo(&Redo, cx); -// assert_eq!(editor.text(cx), "12cde6"); -// assert_eq!(editor.selections.ranges(cx), vec![5..5]); - -// // Redo the last transaction on its own. -// editor.redo(&Redo, cx); -// assert_eq!(editor.text(cx), "ab2cde6"); -// assert_eq!(editor.selections.ranges(cx), vec![6..6]); - -// // Test empty transactions. -// editor.start_transaction_at(now, cx); -// editor.end_transaction_at(now, cx); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "12cde6"); -// }); -// } + let buffer = cx.build_model(|cx| { + let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "abcde"); + // Ensure automatic grouping doesn't occur. + buffer.set_group_interval(Duration::ZERO); + buffer + }); -// #[gpui::test] -// fn test_ime_composition(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.add_window(|cx| { + let mut editor = build_editor(buffer.clone(), cx); + + // Start a new IME composition. + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); + assert_eq!(editor.text(cx), "äbcde"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) + ); + + // Finalize IME composition. + editor.replace_text_in_range(None, "ā", cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // IME composition edits are grouped and are undone/redone at once. + editor.undo(&Default::default(), cx); + assert_eq!(editor.text(cx), "abcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + editor.redo(&Default::default(), cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition. + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) + ); + + // Undoing during an IME composition cancels it. + editor.undo(&Default::default(), cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition with an invalid marked range, ensuring it gets clipped. + editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); + assert_eq!(editor.text(cx), "ābcdè"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(4)..OffsetUtf16(5)]) + ); + + // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. + editor.replace_text_in_range(Some(4..999), "ę", cx); + assert_eq!(editor.text(cx), "ābcdę"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition with multiple cursors. + editor.change_selections(None, cx, |s| { + s.select_ranges([ + OffsetUtf16(1)..OffsetUtf16(1), + OffsetUtf16(3)..OffsetUtf16(3), + OffsetUtf16(5)..OffsetUtf16(5), + ]) + }); + editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); + assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![ + OffsetUtf16(0)..OffsetUtf16(3), + OffsetUtf16(4)..OffsetUtf16(7), + OffsetUtf16(8)..OffsetUtf16(11) + ]) + ); + + // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. + editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); + assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![ + OffsetUtf16(1)..OffsetUtf16(2), + OffsetUtf16(5)..OffsetUtf16(6), + OffsetUtf16(9)..OffsetUtf16(10) + ]) + ); + + // Finalize IME composition with multiple cursors. + editor.replace_text_in_range(Some(9..10), "2", cx); + assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); + assert_eq!(editor.marked_text_ranges(cx), None); + + editor + }); +} -// let buffer = cx.add_model(|cx| { -// let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde"); -// // Ensure automatic grouping doesn't occur. -// buffer.set_group_interval(Duration::ZERO); -// buffer -// }); +#[gpui::test] +fn test_selection_with_mouse(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// cx.add_window(|cx| { -// let mut editor = build_editor(buffer.clone(), cx); + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); + build_editor(buffer, cx) + }); -// // Start a new IME composition. -// editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); -// editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); -// editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); -// assert_eq!(editor.text(cx), "äbcde"); -// assert_eq!( -// editor.marked_text_ranges(cx), -// Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) -// ); + editor.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); + }); + assert_eq!( + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); + + editor.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); + }); -// // Finalize IME composition. -// editor.replace_text_in_range(None, "ā", cx); -// assert_eq!(editor.text(cx), "ābcde"); -// assert_eq!(editor.marked_text_ranges(cx), None); + assert_eq!( + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); -// // IME composition edits are grouped and are undone/redone at once. -// editor.undo(&Default::default(), cx); -// assert_eq!(editor.text(cx), "abcde"); -// assert_eq!(editor.marked_text_ranges(cx), None); -// editor.redo(&Default::default(), cx); -// assert_eq!(editor.text(cx), "ābcde"); -// assert_eq!(editor.marked_text_ranges(cx), None); - -// // Start a new IME composition. -// editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); -// assert_eq!( -// editor.marked_text_ranges(cx), -// Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) -// ); + editor.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); + }); -// // Undoing during an IME composition cancels it. -// editor.undo(&Default::default(), cx); -// assert_eq!(editor.text(cx), "ābcde"); -// assert_eq!(editor.marked_text_ranges(cx), None); + assert_eq!( + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); -// // Start a new IME composition with an invalid marked range, ensuring it gets clipped. -// editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); -// assert_eq!(editor.text(cx), "ābcdè"); -// assert_eq!( -// editor.marked_text_ranges(cx), -// Some(vec![OffsetUtf16(4)..OffsetUtf16(5)]) -// ); + editor.update(cx, |view, cx| { + view.end_selection(cx); + view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); + }); -// // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. -// editor.replace_text_in_range(Some(4..999), "ę", cx); -// assert_eq!(editor.text(cx), "ābcdę"); -// assert_eq!(editor.marked_text_ranges(cx), None); + assert_eq!( + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); -// // Start a new IME composition with multiple cursors. -// editor.change_selections(None, cx, |s| { -// s.select_ranges([ -// OffsetUtf16(1)..OffsetUtf16(1), -// OffsetUtf16(3)..OffsetUtf16(3), -// OffsetUtf16(5)..OffsetUtf16(5), -// ]) -// }); -// editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); -// assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); -// assert_eq!( -// editor.marked_text_ranges(cx), -// Some(vec![ -// OffsetUtf16(0)..OffsetUtf16(3), -// OffsetUtf16(4)..OffsetUtf16(7), -// OffsetUtf16(8)..OffsetUtf16(11) -// ]) -// ); + editor.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); + view.update_selection(DisplayPoint::new(0, 0), 0, gpui::Point::::zero(), cx); + }); -// // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. -// editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); -// assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); -// assert_eq!( -// editor.marked_text_ranges(cx), -// Some(vec![ -// OffsetUtf16(1)..OffsetUtf16(2), -// OffsetUtf16(5)..OffsetUtf16(6), -// OffsetUtf16(9)..OffsetUtf16(10) -// ]) -// ); + assert_eq!( + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), + [ + DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) + ] + ); + + editor.update(cx, |view, cx| { + view.end_selection(cx); + }); -// // Finalize IME composition with multiple cursors. -// editor.replace_text_in_range(Some(9..10), "2", cx); -// assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); -// assert_eq!(editor.marked_text_ranges(cx), None); + assert_eq!( + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), + [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] + ); +} -// editor -// }); -// } +#[gpui::test] +fn test_canceling_pending_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// #[gpui::test] -// fn test_selection_with_mouse(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }); -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// editor.update(cx, |view, cx| { -// view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); -// }); -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] -// ); + view.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); + }); -// editor.update(cx, |view, cx| { -// view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); -// }); + view.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + }); -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] -// ); + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + }); +} -// editor.update(cx, |view, cx| { -// view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); -// }); +#[gpui::test] +fn test_clone(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let (text, selection_ranges) = marked_text_ranges( + indoc! {" + one + two + threeˇ + four + fiveˇ + "}, + true, + ); + + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&text, cx); + build_editor(buffer, cx) + }); -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] -// ); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); + editor.fold_ranges( + [ + Point::new(1, 0)..Point::new(2, 0), + Point::new(3, 0)..Point::new(4, 0), + ], + true, + cx, + ); + }); -// editor.update(cx, |view, cx| { -// view.end_selection(cx); -// view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); -// }); + let cloned_editor = editor + .update(cx, |editor, cx| { + cx.open_window(Default::default(), |cx| { + cx.build_view(|cx| editor.clone(cx)) + }) + }) + .unwrap(); + + let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap(); + let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)).unwrap(); + + assert_eq!( + cloned_editor + .update(cx, |e, cx| e.display_text(cx)) + .unwrap(), + editor.update(cx, |e, cx| e.display_text(cx)).unwrap() + ); + assert_eq!( + cloned_snapshot + .folds_in_range(0..text.len()) + .collect::>(), + snapshot.folds_in_range(0..text.len()).collect::>(), + ); + assert_set_eq!( + cloned_editor + .update(cx, |editor, cx| editor.selections.ranges::(cx)) + .unwrap(), + editor + .update(cx, |editor, cx| editor.selections.ranges(cx)) + .unwrap() + ); + assert_set_eq!( + cloned_editor + .update(cx, |e, cx| e.selections.display_ranges(cx)) + .unwrap(), + editor + .update(cx, |e, cx| e.selections.display_ranges(cx)) + .unwrap() + ); +} -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] -// ); +//todo!(editor navigate) +// #[gpui::test] +// async fn test_navigation_history(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); -// editor.update(cx, |view, cx| { -// view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); -// view.update_selection(DisplayPoint::new(0, 0), 0, Point::zero(), cx); -// }); +// use workspace::item::Item; -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [ -// DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), -// DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) -// ] -// ); +// 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(); -// editor.update(cx, |view, cx| { -// view.end_selection(cx); -// }); +// 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))); -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] -// ); -// } +// fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { +// editor.nav_history.as_mut().unwrap().pop_backward(cx) +// } -// #[gpui::test] -// fn test_canceling_pending_selection(cx: &mut TestAppContext) { -// init_test(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()); -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); +// // 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()); -// view.update(cx, |view, cx| { -// view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] -// ); -// }); +// // 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()); -// view.update(cx, |view, cx| { -// view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] -// ); -// }); +// // 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::::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::::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) +// ); -// view.update(cx, |view, cx| { -// view.cancel(&Cancel, cx); -// view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] -// ); +// editor +// }) // }); // } -// #[gpui::test] -// fn test_clone(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +fn test_cancel(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// let (text, selection_ranges) = marked_text_ranges( -// indoc! {" -// one -// two -// threeˇ -// four -// fiveˇ -// "}, -// true, -// ); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }); -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&text, cx); -// build_editor(buffer, cx) -// }) -// .root(cx); + view.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); + view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); + view.end_selection(cx); + + view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); + view.update_selection(DisplayPoint::new(0, 3), 0, gpui::Point::::zero(), cx); + view.end_selection(cx); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), + ] + ); + }); -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); -// editor.fold_ranges( -// [ -// Point::new(1, 0)..Point::new(2, 0), -// Point::new(3, 0)..Point::new(4, 0), -// ], -// true, -// cx, -// ); -// }); + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] + ); + }); -// let cloned_editor = editor -// .update(cx, |editor, cx| { -// cx.add_window(Default::default(), |cx| editor.clone(cx)) -// }) -// .root(cx); + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] + ); + }); +} -// let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); -// let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); +#[gpui::test] +fn test_fold_action(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple( + &" + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() { + 2 + } + + fn c() { + 3 + } + } + " + .unindent(), + cx, + ); + build_editor(buffer.clone(), cx) + }); -// assert_eq!( -// cloned_editor.update(cx, |e, cx| e.display_text(cx)), -// editor.update(cx, |e, cx| e.display_text(cx)) -// ); -// assert_eq!( -// cloned_snapshot -// .folds_in_range(0..text.len()) -// .collect::>(), -// snapshot.folds_in_range(0..text.len()).collect::>(), -// ); -// assert_set_eq!( -// cloned_editor.read_with(cx, |editor, cx| editor.selections.ranges::(cx)), -// editor.read_with(cx, |editor, cx| editor.selections.ranges(cx)) -// ); -// assert_set_eq!( -// cloned_editor.update(cx, |e, cx| e.selections.display_ranges(cx)), -// editor.update(cx, |e, cx| e.selections.display_ranges(cx)) -// ); -// } + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]); + }); + view.fold(&Fold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {⋯ + } + + fn c() {⋯ + } + } + " + .unindent(), + ); + + view.fold(&Fold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo {⋯ + } + " + .unindent(), + ); + + view.unfold_lines(&UnfoldLines, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {⋯ + } + + fn c() {⋯ + } + } + " + .unindent(), + ); + + view.unfold_lines(&UnfoldLines, cx); + assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); + }); +} -// #[gpui::test] -// async fn test_navigation_history(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +fn test_move_cursor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); + let view = cx.add_window(|cx| build_editor(buffer.clone(), cx)); + + buffer.update(cx, |buffer, cx| { + buffer.edit( + vec![ + (Point::new(1, 0)..Point::new(1, 0), "\t"), + (Point::new(1, 1)..Point::new(1, 1), "\t"), + ], + None, + cx, + ); + }); + view.update(cx, |view, cx| { + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] + ); + + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.move_to_end(&MoveToEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] + ); + + view.move_to_beginning(&MoveToBeginning, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]); + }); + view.select_to_beginning(&SelectToBeginning, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] + ); + + view.select_to_end(&SelectToEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] + ); + }); +} -// cx.set_global(DragAndDrop::::default()); -// use workspace::item::Item; +#[gpui::test] +fn test_move_cursor_multibyte(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// let fs = FakeFs::new(cx.background()); -// let project = Project::test(fs, [], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); -// window.add_view(cx, |cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); -// let mut editor = build_editor(buffer.clone(), cx); -// let handle = cx.handle(); -// editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); + build_editor(buffer.clone(), cx) + }); -// fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { -// editor.nav_history.as_mut().unwrap().pop_backward(cx) -// } + assert_eq!('ⓐ'.len_utf8(), 3); + assert_eq!('α'.len_utf8(), 2); + + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 6)..Point::new(0, 12), + Point::new(1, 2)..Point::new(1, 4), + Point::new(2, 4)..Point::new(2, 8), + ], + true, + cx, + ); + assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε"); + + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐⓑ⋯".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "a".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "α".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ⋯".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ⋯ε".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ⋯ε".len())] + ); + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐ".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "".len())] + ); + }); +} -// // 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()); +//todo!(finish editor tests) +// #[gpui::test] +// fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); -// // 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 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())]); // }); -// let nav_entry = pop_history(&mut editor, cx).unwrap(); -// editor.navigate(nav_entry.data.unwrap(), cx); -// assert_eq!(nav_entry.item.id(), cx.view_id()); +// view.move_down(&MoveDown, cx); // assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] +// view.selections.display_ranges(cx), +// &[empty_range(1, "abcd".len())] // ); -// 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); +// view.move_down(&MoveDown, cx); // assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] +// view.selections.display_ranges(cx), +// &[empty_range(2, "αβγ".len())] // ); -// assert!(pop_history(&mut editor, cx).is_none()); -// // Move the cursor a large distance via the mouse. -// // The history can jump back to the previous position. -// editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); -// editor.end_selection(cx); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] -// ); -// let nav_entry = pop_history(&mut editor, cx).unwrap(); -// editor.navigate(nav_entry.data.unwrap(), cx); -// assert_eq!(nav_entry.item.id(), cx.view_id()); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] -// ); -// assert!(pop_history(&mut editor, cx).is_none()); - -// // Set scroll position to check later -// editor.set_scroll_position(Point::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(Point::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)] -// ); +// view.move_down(&MoveDown, cx); // assert_eq!( -// editor.scroll_position(cx), -// vec2f(0., editor.max_point(cx).row() as f32) +// view.selections.display_ranges(cx), +// &[empty_range(3, "abcd".len())] // ); -// editor -// }); -// } - -// #[gpui::test] -// fn test_cancel(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); -// view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); -// view.end_selection(cx); - -// view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); -// view.update_selection(DisplayPoint::new(0, 3), 0, Point::zero(), cx); -// view.end_selection(cx); +// view.move_down(&MoveDown, cx); // assert_eq!( // view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), -// DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), -// ] +// &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] // ); -// }); -// view.update(cx, |view, cx| { -// view.cancel(&Cancel, cx); +// view.move_up(&MoveUp, cx); // assert_eq!( // view.selections.display_ranges(cx), -// [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] +// &[empty_range(3, "abcd".len())] // ); -// }); -// view.update(cx, |view, cx| { -// view.cancel(&Cancel, cx); +// view.move_up(&MoveUp, cx); // assert_eq!( // view.selections.display_ranges(cx), -// [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] +// &[empty_range(2, "αβγ".len())] // ); // }); // } -// #[gpui::test] -// fn test_fold_action(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +fn test_beginning_end_of_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple( -// &" -// impl Foo { -// // Hello! - -// fn a() { -// 1 -// } + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\n def", cx); + build_editor(buffer, cx) + }); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ]); + }); + }); -// fn b() { -// 2 -// } + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); + }); -// fn c() { -// 3 -// } -// } -// " -// .unindent(), -// cx, -// ); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]); -// }); -// view.fold(&Fold, cx); -// assert_eq!( -// view.display_text(cx), -// " -// impl Foo { -// // Hello! + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); + }); -// fn a() { -// 1 -// } + view.update(cx, |view, cx| { + view.move_to_end_of_line(&MoveToEndOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + }); -// fn b() {⋯ -// } + // Moving to the end of line again is a no-op. + view.update(cx, |view, cx| { + view.move_to_end_of_line(&MoveToEndOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + }); -// fn c() {⋯ -// } -// } -// " -// .unindent(), -// ); + view.update(cx, |view, cx| { + view.move_left(&MoveLeft, cx); + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + }); -// view.fold(&Fold, cx); -// assert_eq!( -// view.display_text(cx), -// " -// impl Foo {⋯ -// } -// " -// .unindent(), -// ); + view.update(cx, |view, cx| { + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), + ] + ); + }); -// view.unfold_lines(&UnfoldLines, cx); -// assert_eq!( -// view.display_text(cx), -// " -// impl Foo { -// // Hello! + view.update(cx, |view, cx| { + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + }); -// fn a() { -// 1 -// } + view.update(cx, |view, cx| { + view.select_to_end_of_line( + &SelectToEndOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), + ] + ); + }); -// fn b() {⋯ -// } + view.update(cx, |view, cx| { + view.delete_to_end_of_line(&DeleteToEndOfLine, cx); + assert_eq!(view.display_text(cx), "ab\n de"); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ] + ); + }); -// fn c() {⋯ -// } -// } -// " -// .unindent(), -// ); + view.update(cx, |view, cx| { + view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); + assert_eq!(view.display_text(cx), "\n"); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); +} -// view.unfold_lines(&UnfoldLines, cx); -// assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); -// }); -// } +#[gpui::test] +fn test_prev_next_word_boundary(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); + build_editor(buffer, cx) + }); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), + ]) + }); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); + + view.move_right(&MoveRight, cx); + view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); + assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); + + view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); + assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx); + + view.select_to_next_word_end(&SelectToNextWordEnd, cx); + assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); + }); +} + +//todo!(finish editor tests) // #[gpui::test] -// fn test_move_cursor(cx: &mut TestAppContext) { +// fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { // init_test(cx, |_| {}); -// let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); -// let view = cx -// .add_window(|cx| build_editor(buffer.clone(), cx)) -// .root(cx); - -// buffer.update(cx, |buffer, cx| { -// buffer.edit( -// vec![ -// (Point::new(1, 0)..Point::new(1, 0), "\t"), -// (Point::new(1, 1)..Point::new(1, 1), "\t"), -// ], -// None, -// cx, -// ); +// 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| { -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] -// ); -// view.move_down(&MoveDown, cx); +// view.update(cx, |view, cx| { +// view.set_wrap_width(Some(140.0.into()), cx); // assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] +// view.display_text(cx), +// "use one::{\n two::three::\n four::five\n};" // ); -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] -// ); +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]); +// }); -// view.move_left(&MoveLeft, cx); +// view.move_to_next_word_end(&MoveToNextWordEnd, cx); // assert_eq!( // view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] +// &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] // ); -// view.move_up(&MoveUp, cx); +// view.move_to_next_word_end(&MoveToNextWordEnd, cx); // assert_eq!( // view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] +// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] // ); -// view.move_to_end(&MoveToEnd, cx); +// view.move_to_next_word_end(&MoveToNextWordEnd, cx); // assert_eq!( // view.selections.display_ranges(cx), -// &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] +// &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] // ); -// view.move_to_beginning(&MoveToBeginning, cx); +// view.move_to_next_word_end(&MoveToNextWordEnd, cx); // assert_eq!( // view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] +// &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] // ); -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]); -// }); -// view.select_to_beginning(&SelectToBeginning, cx); +// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); // assert_eq!( // view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] +// &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] // ); -// view.select_to_end(&SelectToEnd, cx); +// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); // assert_eq!( // view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] +// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] // ); // }); // } +//todo!(simulate_resize) // #[gpui::test] -// fn test_move_cursor_multibyte(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 view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); +// 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); -// assert_eq!('ⓐ'.len_utf8(), 3); -// assert_eq!('α'.len_utf8(), 2); +// cx.set_state( +// &r#"ˇone +// two -// view.update(cx, |view, cx| { -// view.fold_ranges( -// vec![ -// Point::new(0, 6)..Point::new(0, 12), -// Point::new(1, 2)..Point::new(1, 4), -// Point::new(2, 4)..Point::new(2, 8), -// ], -// true, -// cx, -// ); -// assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε"); +// three +// fourˇ +// five -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "ⓐ".len())] -// ); -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "ⓐⓑ".len())] -// ); -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "ⓐⓑ⋯".len())] -// ); +// six"# +// .unindent(), +// ); -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "ab⋯e".len())] -// ); -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "ab⋯".len())] -// ); -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "ab".len())] -// ); -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "a".len())] -// ); +// 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(), +// ); -// view.move_down(&MoveDown, cx); +// 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!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "α".len())] +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 0.) // ); -// view.move_right(&MoveRight, cx); +// editor.scroll_screen(&ScrollAmount::Page(1.), cx); // assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβ".len())] +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 3.) // ); -// view.move_right(&MoveRight, cx); +// editor.scroll_screen(&ScrollAmount::Page(1.), cx); // assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβ⋯".len())] +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 6.) // ); -// view.move_right(&MoveRight, cx); +// editor.scroll_screen(&ScrollAmount::Page(-1.), cx); // assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβ⋯ε".len())] +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 3.) // ); -// view.move_up(&MoveUp, cx); +// editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); // assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "ab⋯e".len())] +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 1.) // ); -// view.move_down(&MoveDown, cx); +// editor.scroll_screen(&ScrollAmount::Page(0.5), cx); // assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβ⋯ε".len())] -// ); -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "ab⋯e".len())] -// ); - -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "ⓐⓑ".len())] -// ); -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "ⓐ".len())] -// ); -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "".len())] -// ); -// }); -// } - -// #[gpui::test] -// fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); -// }); -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "abcd".len())] -// ); - -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβγ".len())] -// ); - -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(3, "abcd".len())] -// ); - -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] -// ); - -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(3, "abcd".len())] -// ); - -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβγ".len())] +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 3.) // ); // }); // } // #[gpui::test] -// fn test_beginning_end_of_line(cx: &mut TestAppContext) { +// async fn test_autoscroll(cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); +// let mut cx = EditorTestContext::new(cx).await; -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\n def", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), -// ]); -// }); -// }); - -// view.update(cx, |view, cx| { -// view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), -// ] -// ); +// let line_height = cx.update_editor(|editor, cx| { +// editor.set_vertical_scroll_margin(2, cx); +// editor.style(cx).text.line_height(cx.font_cache()) // }); -// view.update(cx, |view, cx| { -// view.move_to_end_of_line(&MoveToEndOfLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), -// ] -// ); -// }); +// let window = cx.window; +// window.simulate_resize(gpui::Point::new(1000., 6.0 * line_height), &mut cx); -// // Moving to the end of line again is a no-op. -// view.update(cx, |view, cx| { -// view.move_to_end_of_line(&MoveToEndOfLine, cx); +// cx.set_state( +// &r#"ˇone +// two +// three +// four +// five +// six +// seven +// eight +// nine +// ten +// "#, +// ); +// cx.update_editor(|editor, cx| { // assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), -// ] +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 0.0) // ); // }); -// view.update(cx, |view, cx| { -// view.move_left(&MoveLeft, cx); -// view.select_to_beginning_of_line( -// &SelectToBeginningOfLine { -// stop_at_soft_wraps: true, -// }, -// cx, -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), -// ] -// ); +// // 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), +// ]); +// }) // }); - -// view.update(cx, |view, cx| { -// view.select_to_beginning_of_line( -// &SelectToBeginningOfLine { -// stop_at_soft_wraps: true, -// }, -// cx, -// ); +// cx.update_editor(|editor, cx| { // assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), -// ] +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 3.0) // ); // }); -// view.update(cx, |view, cx| { -// view.select_to_beginning_of_line( -// &SelectToBeginningOfLine { -// stop_at_soft_wraps: true, -// }, -// cx, -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), -// ] -// ); +// // Move down. The editor cursor scrolls down to track the newest cursor. +// cx.update_editor(|editor, cx| { +// editor.move_down(&Default::default(), cx); // }); - -// view.update(cx, |view, cx| { -// view.select_to_end_of_line( -// &SelectToEndOfLine { -// stop_at_soft_wraps: true, -// }, -// cx, -// ); +// cx.update_editor(|editor, cx| { // assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), -// ] +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 4.0) // ); // }); -// view.update(cx, |view, cx| { -// view.delete_to_end_of_line(&DeleteToEndOfLine, cx); -// assert_eq!(view.display_text(cx), "ab\n de"); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), -// ] -// ); +// // 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), +// ]); +// }) // }); - -// view.update(cx, |view, cx| { -// view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); -// assert_eq!(view.display_text(cx), "\n"); +// cx.update_editor(|editor, cx| { // assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), -// ] +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 1.0) // ); // }); // } // #[gpui::test] -// fn test_prev_next_word_boundary(cx: &mut TestAppContext) { +// async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); +// let mut cx = EditorTestContext::new(cx).await; -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), -// DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), -// ]) -// }); - -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); - -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx); - -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx); +// 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); -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); +// cx.set_state( +// &r#" +// ˇone +// two +// threeˇ +// four +// five +// six +// seven +// eight +// nine +// ten +// "# +// .unindent(), +// ); -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx); +// 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(), +// ); -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx); +// 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(), +// ); -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); - -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); - -// view.move_right(&MoveRight, cx); -// view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); -// assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); +// 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(), +// ); -// view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); -// assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx); +// 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(), +// ); -// view.select_to_next_word_end(&SelectToNextWordEnd, cx); -// assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); +// // 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] -// fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[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"); + }); +} -// let view = cx -// .add_window(|cx| { -// let buffer = -// MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); +#[gpui::test] +fn test_delete_to_word_boundary(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// view.update(cx, |view, cx| { -// view.set_wrap_width(Some(140.), cx); -// assert_eq!( -// view.display_text(cx), -// "use one::{\n two::three::\n four::five\n};" -// ); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("one two three four", cx); + build_editor(buffer.clone(), cx) + }); -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]); -// }); + 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.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] -// ); + 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"); + }); +} -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] -// ); +#[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(2, 4)..DisplayPoint::new(2, 4)] -// ); + 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(2, 8)..DisplayPoint::new(2, 8)] -// ); + 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_previous_word_start(&MoveToPreviousWordStart, 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_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] -// ); -// }); -// } +#[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 + }); -// #[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; + editor.update(cx, |editor, cx| { + // Edit the buffer directly, deleting ranges surrounding the editor's selections + editor.buffer.update(cx, |buffer, cx| { + buffer.edit( + [ + (Point::new(1, 2)..Point::new(3, 0), ""), + (Point::new(4, 2)..Point::new(6, 0), ""), + ], + None, + cx, + ); + assert_eq!( + buffer.read(cx).text(), + " + a + b() + c() + " + .unindent() + ); + }); + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(1, 2)..Point::new(1, 2), + Point::new(2, 2)..Point::new(2, 2), + ], + ); + + editor.newline(&Newline, cx); + assert_eq!( + editor.text(cx), + " + a + b( + ) + c( + ) + " + .unindent() + ); + + // The selections are moved after the inserted newlines + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(2, 0)..Point::new(2, 0), + Point::new(4, 0)..Point::new(4, 0), + ], + ); + }); +} -// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); -// let window = cx.window; -// window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); +#[gpui::test] +async fn test_newline_above(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); -// cx.set_state( -// &r#"ˇone -// two + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "(" ")" @end) @indent"#) + .unwrap(), + ); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + const a: ˇA = ( + (ˇ + «const_functionˇ»(ˇ), + so«mˇ»et«hˇ»ing_ˇelse,ˇ + )ˇ + ˇ);ˇ + "}); + + cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); + cx.assert_editor_state(indoc! {" + ˇ + const a: A = ( + ˇ + ( + ˇ + ˇ + const_function(), + ˇ + ˇ + ˇ + ˇ + something_else, + ˇ + ) + ˇ + ˇ + ); + "}); +} -// three -// fourˇ -// five +#[gpui::test] +async fn test_newline_below(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); -// six"# -// .unindent(), -// ); + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "(" ")" @end) @indent"#) + .unwrap(), + ); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + const a: ˇA = ( + (ˇ + «const_functionˇ»(ˇ), + so«mˇ»et«hˇ»ing_ˇelse,ˇ + )ˇ + ˇ);ˇ + "}); + + cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); + cx.assert_editor_state(indoc! {" + const a: A = ( + ˇ + ( + ˇ + const_function(), + ˇ + ˇ + something_else, + ˇ + ˇ + ˇ + ˇ + ) + ˇ + ); + ˇ + ˇ + "}); +} -// 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(), -// ); +#[gpui::test] +async fn test_newline_comments(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); -// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("//".into()), + ..LanguageConfig::default() + }, + None, + )); + { + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + // Fooˇ + "}); + + cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.assert_editor_state(indoc! {" + // Foo + //ˇ + "}); + // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix. + cx.set_state(indoc! {" + ˇ// Foo + "}); + cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.assert_editor_state(indoc! {" + + ˇ// Foo + "}); + } + // Ensure that comment continuations can be disabled. + update_test_language_settings(cx, |settings| { + settings.defaults.extend_comment_on_newline = Some(false); + }); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + // Fooˇ + "}); + cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.assert_editor_state(indoc! {" + // Foo + ˇ + "}); +} -// three -// four -// five -// ˇ -// sixˇ"# -// .unindent(), -// ); +#[gpui::test] +fn test_insert_with_old_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); + let mut editor = build_editor(buffer.clone(), cx); + editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); + editor + }); -// three -// four -// five + editor.update(cx, |editor, cx| { + // Edit the buffer directly, deleting ranges surrounding the editor's selections + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); + assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); + }); + assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],); -// sixˇ"# -// .unindent(), -// ); + editor.insert("Z", cx); + assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); -// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two + // The selections are moved after the inserted characters + assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],); + }); +} -// three -// four -// five -// ˇ -// six"# -// .unindent(), -// ); +#[gpui::test] +async fn test_tab(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(3) + }); -// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two -// ˇ -// three -// four -// five + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + ˇabˇc + ˇ🏀ˇ🏀ˇefg + dˇ + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + ˇab ˇc + ˇ🏀 ˇ🏀 ˇefg + d ˇ + "}); + + cx.set_state(indoc! {" + a + «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + a + «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» + "}); +} -// six"# -// .unindent(), -// ); +#[gpui::test] +async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "(" ")" @end) @indent"#) + .unwrap(), + ); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // cursors that are already at the suggested indent level insert + // a soft tab. cursors that are to the left of the suggested indent + // auto-indent their line. + cx.set_state(indoc! {" + ˇ + const a: B = ( + c( + d( + ˇ + ) + ˇ + ˇ ) + ); + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + ˇ + const a: B = ( + c( + d( + ˇ + ) + ˇ + ˇ) + ); + "}); + + // handle auto-indent when there are multiple cursors on the same line + cx.set_state(indoc! {" + const a: B = ( + c( + ˇ ˇ + ˇ ) + ); + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c( + ˇ + ˇ) + ); + "}); +} -// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"ˇone -// two +#[gpui::test] +async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); -// three -// four -// five + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "{" "}" @end) @indent"#) + .unwrap(), + ); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + fn a() { + if b { + \t ˇc + } + } + "}); + + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + fn a() { + if b { + ˇc + } + } + "}); +} -// six"# -// .unindent(), -// ); -// } +#[gpui::test] +async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4); + }); -// #[gpui::test] -// async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; -// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); -// let window = cx.window; -// window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx); + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + + // select across line ending + cx.set_state(indoc! {" + one two + t«hree + ˇ» four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + t«hree + ˇ» four + "}); + + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + t«hree + ˇ» four + "}); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + cx.set_state(indoc! {" + one two + ˇthree + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); + + cx.set_state(indoc! {" + one two + ˇ three + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); +} -// cx.set_state( -// &r#"ˇone -// two -// three -// four -// five -// six -// seven -// eight -// nine -// ten -// "#, -// ); +#[gpui::test] +async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.hard_tabs = Some(true); + }); -// cx.update_editor(|editor, cx| { -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)); -// editor.scroll_screen(&ScrollAmount::Page(1.), cx); -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); -// editor.scroll_screen(&ScrollAmount::Page(1.), cx); -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.)); -// editor.scroll_screen(&ScrollAmount::Page(-1.), cx); -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + let mut cx = EditorTestContext::new(cx).await; + + // select two ranges on one line + cx.set_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + \t«oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + \t\t«oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + \t«oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + + // select across a line ending + cx.set_state(indoc! {" + one two + t«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \tt«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \t\tt«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + \tt«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + t«hree + ˇ»four + "}); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + cx.set_state(indoc! {" + one two + ˇthree + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \tˇthree + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); +} -// editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.)); -// editor.scroll_screen(&ScrollAmount::Page(0.5), cx); -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); -// }); -// } +#[gpui::test] +fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.languages.extend([ + ( + "TOML".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(2), + ..Default::default() + }, + ), + ( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(4), + ..Default::default() + }, + ), + ]); + }); -// #[gpui::test] -// async fn test_autoscroll(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; + let toml_language = Arc::new(Language::new( + LanguageConfig { + name: "TOML".into(), + ..Default::default() + }, + None, + )); + let rust_language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + ..Default::default() + }, + None, + )); + + let toml_buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n").with_language(toml_language, cx) + }); + let rust_buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), "const c: usize = 3;\n") + .with_language(rust_language, cx) + }); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + toml_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + rust_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer + }); -// let line_height = cx.update_editor(|editor, cx| { -// editor.set_vertical_scroll_margin(2, cx); -// editor.style(cx).text.line_height(cx.font_cache()) -// }); - -// let window = cx.window; -// window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx); - -// cx.set_state( -// &r#"ˇone -// two -// three -// four -// five -// six -// seven -// eight -// nine -// ten -// "#, -// ); -// cx.update_editor(|editor, cx| { -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0)); -// }); - -// // Add a cursor below the visible area. Since both cursors cannot fit -// // on screen, the editor autoscrolls to reveal the newest cursor, and -// // allows the vertical scroll margin below that cursor. -// cx.update_editor(|editor, cx| { -// editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { -// selections.select_ranges([ -// Point::new(0, 0)..Point::new(0, 0), -// Point::new(6, 0)..Point::new(6, 0), -// ]); -// }) -// }); -// cx.update_editor(|editor, cx| { -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0)); -// }); - -// // Move down. The editor cursor scrolls down to track the newest cursor. -// cx.update_editor(|editor, cx| { -// editor.move_down(&Default::default(), cx); -// }); -// cx.update_editor(|editor, cx| { -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0)); -// }); - -// // Add a cursor above the visible area. Since both cursors fit on screen, -// // the editor scrolls to show both. -// cx.update_editor(|editor, cx| { -// editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { -// selections.select_ranges([ -// Point::new(1, 0)..Point::new(1, 0), -// Point::new(6, 0)..Point::new(6, 0), -// ]); -// }) -// }); -// cx.update_editor(|editor, cx| { -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0)); -// }); -// } - -// #[gpui::test] -// async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; - -// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); -// let window = cx.window; -// window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); - -// cx.set_state( -// &r#" -// ˇone -// two -// threeˇ -// four -// five -// six -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// ˇfour -// five -// sixˇ -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// four -// five -// six -// ˇseven -// eight -// nineˇ -// ten -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// ˇfour -// five -// sixˇ -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); -// cx.assert_editor_state( -// &r#" -// ˇone -// two -// threeˇ -// four -// five -// six -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); - -// // Test select collapsing -// cx.update_editor(|editor, cx| { -// editor.move_page_down(&MovePageDown::default(), cx); -// editor.move_page_down(&MovePageDown::default(), cx); -// editor.move_page_down(&MovePageDown::default(), cx); -// }); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// four -// five -// six -// seven -// eight -// nine -// ˇten -// ˇ"# -// .unindent(), -// ); -// } - -// #[gpui::test] -// async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state("one «two threeˇ» four"); -// cx.update_editor(|editor, cx| { -// editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); -// assert_eq!(editor.text(cx), " four"); -// }); -// } - -// #[gpui::test] -// fn test_delete_to_word_boundary(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("one two three four", cx); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// // an empty selection - the preceding word fragment is deleted -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// // characters selected - they are deleted -// DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12), -// ]) -// }); -// view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx); -// assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four"); -// }); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// // an empty selection - the following word fragment is deleted -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// // characters selected - they are deleted -// DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10), -// ]) -// }); -// view.delete_to_next_word_end(&DeleteToNextWordEnd, cx); -// assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our"); -// }); -// } - -// #[gpui::test] -// fn test_newline(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), -// DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), -// ]) -// }); - -// view.newline(&Newline, cx); -// assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); -// }); -// } - -// #[gpui::test] -// fn test_newline_with_old_selections(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple( -// " -// a -// b( -// X -// ) -// c( -// X -// ) -// " -// .unindent() -// .as_str(), -// cx, -// ); -// let mut editor = build_editor(buffer.clone(), cx); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([ -// Point::new(2, 4)..Point::new(2, 5), -// Point::new(5, 4)..Point::new(5, 5), -// ]) -// }); -// editor -// }) -// .root(cx); - -// editor.update(cx, |editor, cx| { -// // Edit the buffer directly, deleting ranges surrounding the editor's selections -// editor.buffer.update(cx, |buffer, cx| { -// buffer.edit( -// [ -// (Point::new(1, 2)..Point::new(3, 0), ""), -// (Point::new(4, 2)..Point::new(6, 0), ""), -// ], -// None, -// cx, -// ); -// assert_eq!( -// buffer.read(cx).text(), -// " -// a -// b() -// c() -// " -// .unindent() -// ); -// }); -// assert_eq!( -// editor.selections.ranges(cx), -// &[ -// Point::new(1, 2)..Point::new(1, 2), -// Point::new(2, 2)..Point::new(2, 2), -// ], -// ); - -// editor.newline(&Newline, cx); -// assert_eq!( -// editor.text(cx), -// " -// a -// b( -// ) -// c( -// ) -// " -// .unindent() -// ); - -// // The selections are moved after the inserted newlines -// assert_eq!( -// editor.selections.ranges(cx), -// &[ -// Point::new(2, 0)..Point::new(2, 0), -// Point::new(4, 0)..Point::new(4, 0), -// ], -// ); -// }); -// } - -// #[gpui::test] -// async fn test_newline_above(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(4) -// }); - -// let language = Arc::new( -// Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query(r#"(_ "(" ")" @end) @indent"#) -// .unwrap(), -// ); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); -// cx.set_state(indoc! {" -// const a: ˇA = ( -// (ˇ -// «const_functionˇ»(ˇ), -// so«mˇ»et«hˇ»ing_ˇelse,ˇ -// )ˇ -// ˇ);ˇ -// "}); - -// cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); -// cx.assert_editor_state(indoc! {" -// ˇ -// const a: A = ( -// ˇ -// ( -// ˇ -// ˇ -// const_function(), -// ˇ -// ˇ -// ˇ -// ˇ -// something_else, -// ˇ -// ) -// ˇ -// ˇ -// ); -// "}); -// } - -// #[gpui::test] -// async fn test_newline_below(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(4) -// }); - -// let language = Arc::new( -// Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query(r#"(_ "(" ")" @end) @indent"#) -// .unwrap(), -// ); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); -// cx.set_state(indoc! {" -// const a: ˇA = ( -// (ˇ -// «const_functionˇ»(ˇ), -// so«mˇ»et«hˇ»ing_ˇelse,ˇ -// )ˇ -// ˇ);ˇ -// "}); - -// cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); -// cx.assert_editor_state(indoc! {" -// const a: A = ( -// ˇ -// ( -// ˇ -// const_function(), -// ˇ -// ˇ -// something_else, -// ˇ -// ˇ -// ˇ -// ˇ -// ) -// ˇ -// ); -// ˇ -// ˇ -// "}); -// } - -// #[gpui::test] -// async fn test_newline_comments(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(4) -// }); - -// let language = Arc::new(Language::new( -// LanguageConfig { -// line_comment: Some("//".into()), -// ..LanguageConfig::default() -// }, -// None, -// )); -// { -// let mut cx = EditorTestContext::new(cx).await; -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); -// cx.set_state(indoc! {" -// // Fooˇ -// "}); - -// cx.update_editor(|e, cx| e.newline(&Newline, cx)); -// cx.assert_editor_state(indoc! {" -// // Foo -// //ˇ -// "}); -// // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix. -// cx.set_state(indoc! {" -// ˇ// Foo -// "}); -// cx.update_editor(|e, cx| e.newline(&Newline, cx)); -// cx.assert_editor_state(indoc! {" - -// ˇ// Foo -// "}); -// } -// // Ensure that comment continuations can be disabled. -// update_test_language_settings(cx, |settings| { -// settings.defaults.extend_comment_on_newline = Some(false); -// }); -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state(indoc! {" -// // Fooˇ -// "}); -// cx.update_editor(|e, cx| e.newline(&Newline, cx)); -// cx.assert_editor_state(indoc! {" -// // Foo -// ˇ -// "}); -// } - -// #[gpui::test] -// fn test_insert_with_old_selections(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); -// let mut editor = build_editor(buffer.clone(), cx); -// editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); -// editor -// }) -// .root(cx); - -// editor.update(cx, |editor, cx| { -// // Edit the buffer directly, deleting ranges surrounding the editor's selections -// editor.buffer.update(cx, |buffer, cx| { -// buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); -// assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); -// }); -// assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],); - -// editor.insert("Z", cx); -// assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); - -// // The selections are moved after the inserted characters -// assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],); -// }); -// } - -// #[gpui::test] -// async fn test_tab(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(3) -// }); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state(indoc! {" -// ˇabˇc -// ˇ🏀ˇ🏀ˇefg -// dˇ -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// ˇab ˇc -// ˇ🏀 ˇ🏀 ˇefg -// d ˇ -// "}); - -// cx.set_state(indoc! {" -// a -// «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// a -// «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» -// "}); -// } - -// #[gpui::test] -// async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; -// let language = Arc::new( -// Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query(r#"(_ "(" ")" @end) @indent"#) -// .unwrap(), -// ); -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - -// // cursors that are already at the suggested indent level insert -// // a soft tab. cursors that are to the left of the suggested indent -// // auto-indent their line. -// cx.set_state(indoc! {" -// ˇ -// const a: B = ( -// c( -// d( -// ˇ -// ) -// ˇ -// ˇ ) -// ); -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// ˇ -// const a: B = ( -// c( -// d( -// ˇ -// ) -// ˇ -// ˇ) -// ); -// "}); - -// // handle auto-indent when there are multiple cursors on the same line -// cx.set_state(indoc! {" -// const a: B = ( -// c( -// ˇ ˇ -// ˇ ) -// ); -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c( -// ˇ -// ˇ) -// ); -// "}); -// } - -// #[gpui::test] -// async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(4) -// }); - -// let language = Arc::new( -// Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query(r#"(_ "{" "}" @end) @indent"#) -// .unwrap(), -// ); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); -// cx.set_state(indoc! {" -// fn a() { -// if b { -// \t ˇc -// } -// } -// "}); - -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// fn a() { -// if b { -// ˇc -// } -// } -// "}); -// } - -// #[gpui::test] -// async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(4); -// }); - -// let mut cx = EditorTestContext::new(cx).await; - -// cx.set_state(indoc! {" -// «oneˇ» «twoˇ» -// three -// four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// «oneˇ» «twoˇ» -// three -// four -// "}); - -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// «oneˇ» «twoˇ» -// three -// four -// "}); - -// // select across line ending -// cx.set_state(indoc! {" -// one two -// t«hree -// ˇ» four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// t«hree -// ˇ» four -// "}); - -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// t«hree -// ˇ» four -// "}); - -// // Ensure that indenting/outdenting works when the cursor is at column 0. -// cx.set_state(indoc! {" -// one two -// ˇthree -// four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// ˇthree -// four -// "}); - -// cx.set_state(indoc! {" -// one two -// ˇ three -// four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// ˇthree -// four -// "}); -// } - -// #[gpui::test] -// async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.hard_tabs = Some(true); -// }); - -// let mut cx = EditorTestContext::new(cx).await; - -// // select two ranges on one line -// cx.set_state(indoc! {" -// «oneˇ» «twoˇ» -// three -// four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// \t«oneˇ» «twoˇ» -// three -// four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// \t\t«oneˇ» «twoˇ» -// three -// four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// \t«oneˇ» «twoˇ» -// three -// four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// «oneˇ» «twoˇ» -// three -// four -// "}); - -// // select across a line ending -// cx.set_state(indoc! {" -// one two -// t«hree -// ˇ»four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// \tt«hree -// ˇ»four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// \t\tt«hree -// ˇ»four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// \tt«hree -// ˇ»four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// t«hree -// ˇ»four -// "}); - -// // Ensure that indenting/outdenting works when the cursor is at column 0. -// cx.set_state(indoc! {" -// one two -// ˇthree -// four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// ˇthree -// four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// \tˇthree -// four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// ˇthree -// four -// "}); -// } - -// #[gpui::test] -// fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { -// init_test(cx, |settings| { -// settings.languages.extend([ -// ( -// "TOML".into(), -// LanguageSettingsContent { -// tab_size: NonZeroU32::new(2), -// ..Default::default() -// }, -// ), -// ( -// "Rust".into(), -// LanguageSettingsContent { -// tab_size: NonZeroU32::new(4), -// ..Default::default() -// }, -// ), -// ]); -// }); - -// let toml_language = Arc::new(Language::new( -// LanguageConfig { -// name: "TOML".into(), -// ..Default::default() -// }, -// None, -// )); -// let rust_language = Arc::new(Language::new( -// LanguageConfig { -// name: "Rust".into(), -// ..Default::default() -// }, -// None, -// )); - -// let toml_buffer = cx.add_model(|cx| { -// Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx) -// }); -// let rust_buffer = cx.add_model(|cx| { -// Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") -// .with_language(rust_language, cx) -// }); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// toml_buffer.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }], -// cx, -// ); -// multibuffer.push_excerpts( -// rust_buffer.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 0), -// primary: None, -// }], -// cx, -// ); -// multibuffer -// }); - -// cx.add_window(|cx| { -// let mut editor = build_editor(multibuffer, cx); - -// assert_eq!( -// editor.text(cx), -// indoc! {" -// a = 1 -// b = 2 - -// const c: usize = 3; -// "} -// ); - -// select_ranges( -// &mut editor, -// indoc! {" -// «aˇ» = 1 -// b = 2 - -// «const c:ˇ» usize = 3; -// "}, -// cx, -// ); - -// editor.tab(&Tab, cx); -// assert_text_with_selections( -// &mut editor, -// indoc! {" -// «aˇ» = 1 -// b = 2 - -// «const c:ˇ» usize = 3; -// "}, -// cx, -// ); -// editor.tab_prev(&TabPrev, cx); -// assert_text_with_selections( -// &mut editor, -// indoc! {" -// «aˇ» = 1 -// b = 2 - -// «const c:ˇ» usize = 3; -// "}, -// cx, -// ); - -// editor -// }); -// } - -// #[gpui::test] -// async fn test_backspace(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// // Basic backspace -// cx.set_state(indoc! {" -// onˇe two three -// fou«rˇ» five six -// seven «ˇeight nine -// »ten -// "}); -// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); -// cx.assert_editor_state(indoc! {" -// oˇe two three -// fouˇ five six -// seven ˇten -// "}); - -// // Test backspace inside and around indents -// cx.set_state(indoc! {" -// zero -// ˇone -// ˇtwo -// ˇ ˇ ˇ three -// ˇ ˇ four -// "}); -// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); -// cx.assert_editor_state(indoc! {" -// zero -// ˇone -// ˇtwo -// ˇ threeˇ four -// "}); - -// // Test backspace with line_mode set to true -// cx.update_editor(|e, _| e.selections.line_mode = true); -// cx.set_state(indoc! {" -// The ˇquick ˇbrown -// fox jumps over -// the lazy dog -// ˇThe qu«ick bˇ»rown"}); -// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); -// cx.assert_editor_state(indoc! {" -// ˇfox jumps over -// the lazy dogˇ"}); -// } - -// #[gpui::test] -// async fn test_delete(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state(indoc! {" -// onˇe two three -// fou«rˇ» five six -// seven «ˇeight nine -// »ten -// "}); -// cx.update_editor(|e, cx| e.delete(&Delete, cx)); -// cx.assert_editor_state(indoc! {" -// onˇ two three -// fouˇ five six -// seven ˇten -// "}); - -// // Test backspace with line_mode set to true -// cx.update_editor(|e, _| e.selections.line_mode = true); -// cx.set_state(indoc! {" -// The ˇquick ˇbrown -// fox «ˇjum»ps over -// the lazy dog -// ˇThe qu«ick bˇ»rown"}); -// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); -// cx.assert_editor_state("ˇthe lazy dogˇ"); -// } - -// #[gpui::test] -// fn test_delete_line(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), -// DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), -// ]) -// }); -// view.delete_line(&DeleteLine, cx); -// assert_eq!(view.display_text(cx), "ghi"); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) -// ] -// ); -// }); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) -// }); -// view.delete_line(&DeleteLine, cx); -// assert_eq!(view.display_text(cx), "ghi\n"); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] -// ); -// }); -// } - -// #[gpui::test] -// fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// cx.add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); -// let mut editor = build_editor(buffer.clone(), cx); -// let buffer = buffer.read(cx).as_singleton().unwrap(); - -// assert_eq!( -// editor.selections.ranges::(cx), -// &[Point::new(0, 0)..Point::new(0, 0)] -// ); - -// // When on single line, replace newline at end by space -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); -// assert_eq!( -// editor.selections.ranges::(cx), -// &[Point::new(0, 3)..Point::new(0, 3)] -// ); - -// // When multiple lines are selected, remove newlines that are spanned by the selection -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) -// }); -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n"); -// assert_eq!( -// editor.selections.ranges::(cx), -// &[Point::new(0, 11)..Point::new(0, 11)] -// ); - -// // Undo should be transactional -// editor.undo(&Undo, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); -// assert_eq!( -// editor.selections.ranges::(cx), -// &[Point::new(0, 5)..Point::new(2, 2)] -// ); - -// // When joining an empty line don't insert a space -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) -// }); -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n"); -// assert_eq!( -// editor.selections.ranges::(cx), -// [Point::new(2, 3)..Point::new(2, 3)] -// ); - -// // We can remove trailing newlines -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); -// assert_eq!( -// editor.selections.ranges::(cx), -// [Point::new(2, 3)..Point::new(2, 3)] -// ); - -// // We don't blow up on the last line -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); -// assert_eq!( -// editor.selections.ranges::(cx), -// [Point::new(2, 3)..Point::new(2, 3)] -// ); - -// // reset to test indentation -// editor.buffer.update(cx, |buffer, cx| { -// buffer.edit( -// [ -// (Point::new(1, 0)..Point::new(1, 2), " "), -// (Point::new(2, 0)..Point::new(2, 3), " \n\td"), -// ], -// None, -// cx, -// ) -// }); - -// // We remove any leading spaces -// assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td"); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) -// }); -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td"); - -// // We don't insert a space for a line containing only spaces -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td"); - -// // We ignore any leading tabs -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); - -// editor -// }); -// } - -// #[gpui::test] -// fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// cx.add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); -// let mut editor = build_editor(buffer.clone(), cx); -// let buffer = buffer.read(cx).as_singleton().unwrap(); - -// editor.change_selections(None, cx, |s| { -// s.select_ranges([ -// Point::new(0, 2)..Point::new(1, 1), -// Point::new(1, 2)..Point::new(1, 2), -// Point::new(3, 1)..Point::new(3, 2), -// ]) -// }); - -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n"); - -// assert_eq!( -// editor.selections.ranges::(cx), -// [ -// Point::new(0, 7)..Point::new(0, 7), -// Point::new(1, 3)..Point::new(1, 3) -// ] -// ); -// editor -// }); -// } - -// #[gpui::test] -// async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// // Test sort_lines_case_insensitive() -// cx.set_state(indoc! {" -// «z -// y -// x -// Z -// Y -// Xˇ» -// "}); -// cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx)); -// cx.assert_editor_state(indoc! {" -// «x -// X -// y -// Y -// z -// Zˇ» -// "}); - -// // Test reverse_lines() -// cx.set_state(indoc! {" -// «5 -// 4 -// 3 -// 2 -// 1ˇ» -// "}); -// cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx)); -// cx.assert_editor_state(indoc! {" -// «1 -// 2 -// 3 -// 4 -// 5ˇ» -// "}); - -// // Skip testing shuffle_line() - -// // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive() -// // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines) - -// // Don't manipulate when cursor is on single line, but expand the selection -// cx.set_state(indoc! {" -// ddˇdd -// ccc -// bb -// a -// "}); -// cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); -// cx.assert_editor_state(indoc! {" -// «ddddˇ» -// ccc -// bb -// a -// "}); - -// // Basic manipulate case -// // Start selection moves to column 0 -// // End of selection shrinks to fit shorter line -// cx.set_state(indoc! {" -// dd«d -// ccc -// bb -// aaaaaˇ» -// "}); -// cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); -// cx.assert_editor_state(indoc! {" -// «aaaaa -// bb -// ccc -// dddˇ» -// "}); - -// // Manipulate case with newlines -// cx.set_state(indoc! {" -// dd«d -// ccc - -// bb -// aaaaa - -// ˇ» -// "}); -// cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); -// cx.assert_editor_state(indoc! {" -// « - -// aaaaa -// bb -// ccc -// dddˇ» - -// "}); -// } - -// #[gpui::test] -// async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// // Manipulate with multiple selections on a single line -// cx.set_state(indoc! {" -// dd«dd -// cˇ»c«c -// bb -// aaaˇ»aa -// "}); -// cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); -// cx.assert_editor_state(indoc! {" -// «aaaaa -// bb -// ccc -// ddddˇ» -// "}); - -// // Manipulate with multiple disjoin selections -// cx.set_state(indoc! {" -// 5« -// 4 -// 3 -// 2 -// 1ˇ» - -// dd«dd -// ccc -// bb -// aaaˇ»aa -// "}); -// cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); -// cx.assert_editor_state(indoc! {" -// «1 -// 2 -// 3 -// 4 -// 5ˇ» - -// «aaaaa -// bb -// ccc -// ddddˇ» -// "}); -// } - -// #[gpui::test] -// async fn test_manipulate_text(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// // Test convert_to_upper_case() -// cx.set_state(indoc! {" -// «hello worldˇ» -// "}); -// cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); -// cx.assert_editor_state(indoc! {" -// «HELLO WORLDˇ» -// "}); - -// // Test convert_to_lower_case() -// cx.set_state(indoc! {" -// «HELLO WORLDˇ» -// "}); -// cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx)); -// cx.assert_editor_state(indoc! {" -// «hello worldˇ» -// "}); - -// // Test multiple line, single selection case -// // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary -// cx.set_state(indoc! {" -// «The quick brown -// fox jumps over -// the lazy dogˇ» -// "}); -// cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx)); -// cx.assert_editor_state(indoc! {" -// «The Quick Brown -// Fox Jumps Over -// The Lazy Dogˇ» -// "}); - -// // Test multiple line, single selection case -// // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary -// cx.set_state(indoc! {" -// «The quick brown -// fox jumps over -// the lazy dogˇ» -// "}); -// cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx)); -// cx.assert_editor_state(indoc! {" -// «TheQuickBrown -// FoxJumpsOver -// TheLazyDogˇ» -// "}); - -// // From here on out, test more complex cases of manipulate_text() - -// // Test no selection case - should affect words cursors are in -// // Cursor at beginning, middle, and end of word -// cx.set_state(indoc! {" -// ˇhello big beauˇtiful worldˇ -// "}); -// cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); -// cx.assert_editor_state(indoc! {" -// «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ» -// "}); - -// // Test multiple selections on a single line and across multiple lines -// cx.set_state(indoc! {" -// «Theˇ» quick «brown -// foxˇ» jumps «overˇ» -// the «lazyˇ» dog -// "}); -// cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); -// cx.assert_editor_state(indoc! {" -// «THEˇ» quick «BROWN -// FOXˇ» jumps «OVERˇ» -// the «LAZYˇ» dog -// "}); - -// // Test case where text length grows -// cx.set_state(indoc! {" -// «tschüߡ» -// "}); -// cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); -// cx.assert_editor_state(indoc! {" -// «TSCHÜSSˇ» -// "}); - -// // Test to make sure we don't crash when text shrinks -// cx.set_state(indoc! {" -// aaa_bbbˇ -// "}); -// cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); -// cx.assert_editor_state(indoc! {" -// «aaaBbbˇ» -// "}); - -// // Test to make sure we all aware of the fact that each word can grow and shrink -// // Final selections should be aware of this fact -// cx.set_state(indoc! {" -// aaa_bˇbb bbˇb_ccc ˇccc_ddd -// "}); -// cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); -// cx.assert_editor_state(indoc! {" -// «aaaBbbˇ» «bbbCccˇ» «cccDddˇ» -// "}); -// } - -// #[gpui::test] -// fn test_duplicate_line(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), -// DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), -// ]) -// }); -// view.duplicate_line(&DuplicateLine, cx); -// assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), -// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), -// DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), -// DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), -// ] -// ); -// }); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), -// DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), -// ]) -// }); -// view.duplicate_line(&DuplicateLine, cx); -// assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), -// DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), -// ] -// ); -// }); -// } - -// #[gpui::test] -// fn test_move_line_up_down(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.fold_ranges( -// vec![ -// Point::new(0, 2)..Point::new(1, 2), -// Point::new(2, 3)..Point::new(4, 1), -// Point::new(7, 0)..Point::new(8, 4), -// ], -// true, -// cx, -// ); -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), -// DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), -// DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), -// ]) -// }); -// assert_eq!( -// view.display_text(cx), -// "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj" -// ); - -// view.move_line_up(&MoveLineUp, cx); -// assert_eq!( -// view.display_text(cx), -// "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff" -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), -// DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), -// DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.move_line_down(&MoveLineDown, cx); -// assert_eq!( -// view.display_text(cx), -// "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj" -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), -// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), -// DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), -// DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.move_line_down(&MoveLineDown, cx); -// assert_eq!( -// view.display_text(cx), -// "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj" -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), -// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), -// DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), -// DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.move_line_up(&MoveLineUp, cx); -// assert_eq!( -// view.display_text(cx), -// "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff" -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), -// DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), -// DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) -// ] -// ); -// }); -// } - -// #[gpui::test] -// fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// editor.update(cx, |editor, cx| { -// let snapshot = editor.buffer.read(cx).snapshot(cx); -// editor.insert_blocks( -// [BlockProperties { -// style: BlockStyle::Fixed, -// position: snapshot.anchor_after(Point::new(2, 0)), -// disposition: BlockDisposition::Below, -// height: 1, -// render: Arc::new(|_| Empty::new().into_any()), -// }], -// Some(Autoscroll::fit()), -// cx, -// ); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) -// }); -// editor.move_line_down(&MoveLineDown, cx); -// }); -// } - -// #[gpui::test] -// fn test_transpose(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// _ = cx.add_window(|cx| { -// let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); - -// editor.change_selections(None, cx, |s| s.select_ranges([1..1])); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bac"); -// assert_eq!(editor.selections.ranges(cx), [2..2]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bca"); -// assert_eq!(editor.selections.ranges(cx), [3..3]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bac"); -// assert_eq!(editor.selections.ranges(cx), [3..3]); - -// editor -// }); - -// _ = cx.add_window(|cx| { -// let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); - -// editor.change_selections(None, cx, |s| s.select_ranges([3..3])); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "acb\nde"); -// assert_eq!(editor.selections.ranges(cx), [3..3]); - -// editor.change_selections(None, cx, |s| s.select_ranges([4..4])); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "acbd\ne"); -// assert_eq!(editor.selections.ranges(cx), [5..5]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "acbde\n"); -// assert_eq!(editor.selections.ranges(cx), [6..6]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "acbd\ne"); -// assert_eq!(editor.selections.ranges(cx), [6..6]); - -// editor -// }); - -// _ = cx.add_window(|cx| { -// let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); - -// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bacd\ne"); -// assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bcade\n"); -// assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bcda\ne"); -// assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bcade\n"); -// assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bcaed\n"); -// assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); - -// editor -// }); - -// _ = cx.add_window(|cx| { -// let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); - -// editor.change_selections(None, cx, |s| s.select_ranges([4..4])); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "🏀🍐✋"); -// assert_eq!(editor.selections.ranges(cx), [8..8]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "🏀✋🍐"); -// assert_eq!(editor.selections.ranges(cx), [11..11]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "🏀🍐✋"); -// assert_eq!(editor.selections.ranges(cx), [11..11]); - -// editor -// }); -// } - -// #[gpui::test] -// async fn test_clipboard(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); -// cx.update_editor(|e, cx| e.cut(&Cut, cx)); -// cx.assert_editor_state("ˇtwo ˇfour ˇsix "); - -// // Paste with three cursors. Each cursor pastes one slice of the clipboard text. -// cx.set_state("two ˇfour ˇsix ˇ"); -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ"); - -// // Paste again but with only two cursors. Since the number of cursors doesn't -// // match the number of slices in the clipboard, the entire clipboard text -// // is pasted at each cursor. -// cx.set_state("ˇtwo one✅ four three six five ˇ"); -// cx.update_editor(|e, cx| { -// e.handle_input("( ", cx); -// e.paste(&Paste, cx); -// e.handle_input(") ", cx); -// }); -// cx.assert_editor_state( -// &([ -// "( one✅ ", -// "three ", -// "five ) ˇtwo one✅ four three six five ( one✅ ", -// "three ", -// "five ) ˇ", -// ] -// .join("\n")), -// ); - -// // Cut with three selections, one of which is full-line. -// cx.set_state(indoc! {" -// 1«2ˇ»3 -// 4ˇ567 -// «8ˇ»9"}); -// cx.update_editor(|e, cx| e.cut(&Cut, cx)); -// cx.assert_editor_state(indoc! {" -// 1ˇ3 -// ˇ9"}); - -// // Paste with three selections, noticing how the copied selection that was full-line -// // gets inserted before the second cursor. -// cx.set_state(indoc! {" -// 1ˇ3 -// 9ˇ -// «oˇ»ne"}); -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// 12ˇ3 -// 4567 -// 9ˇ -// 8ˇne"}); - -// // Copy with a single cursor only, which writes the whole line into the clipboard. -// cx.set_state(indoc! {" -// The quick brown -// fox juˇmps over -// the lazy dog"}); -// cx.update_editor(|e, cx| e.copy(&Copy, cx)); -// cx.cx.assert_clipboard_content(Some("fox jumps over\n")); - -// // Paste with three selections, noticing how the copied full-line selection is inserted -// // before the empty selections but replaces the selection that is non-empty. -// cx.set_state(indoc! {" -// Tˇhe quick brown -// «foˇ»x jumps over -// tˇhe lazy dog"}); -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// fox jumps over -// Tˇhe quick brown -// fox jumps over -// ˇx jumps over -// fox jumps over -// tˇhe lazy dog"}); -// } - -// #[gpui::test] -// async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; -// let language = Arc::new(Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// )); -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - -// // Cut an indented block, without the leading whitespace. -// cx.set_state(indoc! {" -// const a: B = ( -// c(), -// «d( -// e, -// f -// )ˇ» -// ); -// "}); -// cx.update_editor(|e, cx| e.cut(&Cut, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c(), -// ˇ -// ); -// "}); - -// // Paste it at the same position. -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c(), -// d( -// e, -// f -// )ˇ -// ); -// "}); - -// // Paste it at a line with a lower indent level. -// cx.set_state(indoc! {" -// ˇ -// const a: B = ( -// c(), -// ); -// "}); -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// d( -// e, -// f -// )ˇ -// const a: B = ( -// c(), -// ); -// "}); - -// // Cut an indented block, with the leading whitespace. -// cx.set_state(indoc! {" -// const a: B = ( -// c(), -// « d( -// e, -// f -// ) -// ˇ»); -// "}); -// cx.update_editor(|e, cx| e.cut(&Cut, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c(), -// ˇ); -// "}); - -// // Paste it at the same position. -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c(), -// d( -// e, -// f -// ) -// ˇ); -// "}); - -// // Paste it at a line with a higher indent level. -// cx.set_state(indoc! {" -// const a: B = ( -// c(), -// d( -// e, -// fˇ -// ) -// ); -// "}); -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c(), -// d( -// e, -// f d( -// e, -// f -// ) -// ˇ -// ) -// ); -// "}); -// } - -// #[gpui::test] -// fn test_select_all(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.select_all(&SelectAll, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] -// ); -// }); -// } - -// #[gpui::test] -// fn test_select_line(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), -// DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), -// ]) -// }); -// view.select_line(&SelectLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), -// DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.select_line(&SelectLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), -// DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), -// ] -// ); -// }); + cx.add_window(|cx| { + let mut editor = build_editor(multibuffer, cx); + + assert_eq!( + editor.text(cx), + indoc! {" + a = 1 + b = 2 + + const c: usize = 3; + "} + ); + + select_ranges( + &mut editor, + indoc! {" + «aˇ» = 1 + b = 2 + + «const c:ˇ» usize = 3; + "}, + cx, + ); + + editor.tab(&Tab, cx); + assert_text_with_selections( + &mut editor, + indoc! {" + «aˇ» = 1 + b = 2 + + «const c:ˇ» usize = 3; + "}, + cx, + ); + editor.tab_prev(&TabPrev, cx); + assert_text_with_selections( + &mut editor, + indoc! {" + «aˇ» = 1 + b = 2 + + «const c:ˇ» usize = 3; + "}, + cx, + ); + + editor + }); +} -// view.update(cx, |view, cx| { -// view.select_line(&SelectLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] -// ); -// }); -// } +#[gpui::test] +async fn test_backspace(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Basic backspace + cx.set_state(indoc! {" + onˇe two three + fou«rˇ» five six + seven «ˇeight nine + »ten + "}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + oˇe two three + fouˇ five six + seven ˇten + "}); + + // Test backspace inside and around indents + cx.set_state(indoc! {" + zero + ˇone + ˇtwo + ˇ ˇ ˇ three + ˇ ˇ four + "}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + zero + ˇone + ˇtwo + ˇ threeˇ four + "}); + + // Test backspace with line_mode set to true + cx.update_editor(|e, _| e.selections.line_mode = true); + cx.set_state(indoc! {" + The ˇquick ˇbrown + fox jumps over + the lazy dog + ˇThe qu«ick bˇ»rown"}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + ˇfox jumps over + the lazy dogˇ"}); +} -// #[gpui::test] -// fn test_split_selection_into_lines(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_delete(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + onˇe two three + fou«rˇ» five six + seven «ˇeight nine + »ten + "}); + cx.update_editor(|e, cx| e.delete(&Delete, cx)); + cx.assert_editor_state(indoc! {" + onˇ two three + fouˇ five six + seven ˇten + "}); + + // Test backspace with line_mode set to true + cx.update_editor(|e, _| e.selections.line_mode = true); + cx.set_state(indoc! {" + The ˇquick ˇbrown + fox «ˇjum»ps over + the lazy dog + ˇThe qu«ick bˇ»rown"}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state("ˇthe lazy dogˇ"); +} -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.fold_ranges( -// vec![ -// Point::new(0, 2)..Point::new(1, 2), -// Point::new(2, 3)..Point::new(4, 1), -// Point::new(7, 0)..Point::new(8, 4), -// ], -// true, -// cx, -// ); -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), -// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), -// ]) -// }); -// assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"); -// }); +#[gpui::test] +fn test_delete_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// view.update(cx, |view, cx| { -// view.split_selection_into_lines(&SplitSelectionIntoLines, cx); -// assert_eq!( -// view.display_text(cx), -// "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), -// DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) -// ] -// ); -// }); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ]) + }); + view.delete_line(&DeleteLine, cx); + assert_eq!(view.display_text(cx), "ghi"); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) + ] + ); + }); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)]) -// }); -// view.split_selection_into_lines(&SplitSelectionIntoLines, cx); -// assert_eq!( -// view.display_text(cx), -// "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), -// DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), -// DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), -// DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), -// DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), -// DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), -// DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), -// DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) -// ] -// ); -// }); -// } + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) + }); + view.delete_line(&DeleteLine, cx); + assert_eq!(view.display_text(cx), "ghi\n"); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] + ); + }); +} +//todo!(select_anchor_ranges) // #[gpui::test] -// fn test_add_selection_above_below(cx: &mut TestAppContext) { +// fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { // init_test(cx, |_| {}); -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]) -// }); -// }); -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] -// ); - -// view.undo_selection(&UndoSelection, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) -// ] -// ); - -// view.redo_selection(&RedoSelection, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), -// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), -// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]) -// }); -// }); -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), -// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), -// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) -// ] -// ); -// }); +// cx.add_window(|cx| { +// let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); +// let mut editor = build_editor(buffer.clone(), cx); +// let buffer = buffer.read(cx).as_singleton().unwrap(); -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); // assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] +// editor.selections.ranges::(cx), +// &[Point::new(0, 0)..Point::new(0, 0)] // ); -// }); -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); +// // When on single line, replace newline at end by space +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); // assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] +// editor.selections.ranges::(cx), +// &[Point::new(0, 3)..Point::new(0, 3)] // ); -// }); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)]) +// // When multiple lines are selected, remove newlines that are spanned by the selection +// editor.change_selections(None, cx, |s| { +// s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) // }); -// view.add_selection_below(&AddSelectionBelow, cx); +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n"); // assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), -// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), -// ] +// editor.selections.ranges::(cx), +// &[Point::new(0, 11)..Point::new(0, 11)] // ); -// }); -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); +// // Undo should be transactional +// editor.undo(&Undo, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); // assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), -// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), -// DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), -// ] +// editor.selections.ranges::(cx), +// &[Point::new(0, 5)..Point::new(2, 2)] // ); -// }); -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); +// // When joining an empty line don't insert a space +// editor.change_selections(None, cx, |s| { +// s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) +// }); +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n"); // assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), -// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), -// ] +// editor.selections.ranges::(cx), +// [Point::new(2, 3)..Point::new(2, 3)] // ); -// }); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)]) -// }); -// }); -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); +// // We can remove trailing newlines +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); // assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), -// DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), -// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), -// ] +// editor.selections.ranges::(cx), +// [Point::new(2, 3)..Point::new(2, 3)] // ); -// }); -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); +// // We don't blow up on the last line +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); // assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), -// DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), -// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), -// ] +// editor.selections.ranges::(cx), +// [Point::new(2, 3)..Point::new(2, 3)] // ); -// }); -// } -// #[gpui::test] -// async fn test_select_next(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); - -// cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); -// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); -// cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); - -// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); -// } - -// #[gpui::test] -// async fn test_select_previous(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// { -// // `Select previous` without a selection (selects wordwise) -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); -// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); -// cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); -// } -// { -// // `Select previous` with a selection -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); - -// cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); - -// cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); -// } -// } - -// #[gpui::test] -// async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let language = Arc::new(Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// )); - -// let text = r#" -// use mod1::mod2::{mod3, mod4}; - -// fn fn_1(param1: bool, param2: &str) { -// let var1 = "text"; -// } -// "# -// .unindent(); - -// let buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) -// .await; - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), -// DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), -// DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), -// ]); +// // reset to test indentation +// editor.buffer.update(cx, |buffer, cx| { +// buffer.edit( +// [ +// (Point::new(1, 0)..Point::new(1, 2), " "), +// (Point::new(2, 0)..Point::new(2, 3), " \n\td"), +// ], +// None, +// cx, +// ) // }); -// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| { view.selections.display_ranges(cx) }), -// &[ -// DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), -// DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), -// DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), -// ] -// ); - -// view.update(cx, |view, cx| { -// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), -// DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), -// ] -// ); - -// view.update(cx, |view, cx| { -// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] -// ); - -// // Trying to expand the selected syntax node one more time has no effect. -// view.update(cx, |view, cx| { -// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] -// ); - -// view.update(cx, |view, cx| { -// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), -// DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), -// ] -// ); -// view.update(cx, |view, cx| { -// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), -// DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), -// DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), -// ] -// ); +// // We remove any leading spaces +// assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td"); +// editor.change_selections(None, cx, |s| { +// s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) +// }); +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td"); -// view.update(cx, |view, cx| { -// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), -// DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), -// DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), -// ] -// ); +// // We don't insert a space for a line containing only spaces +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td"); -// // Trying to shrink the selected syntax node one more time has no effect. -// view.update(cx, |view, cx| { -// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), -// DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), -// DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), -// ] -// ); +// // We ignore any leading tabs +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); -// // Ensure that we keep expanding the selection if the larger selection starts or ends within -// // a fold. -// view.update(cx, |view, cx| { -// view.fold_ranges( -// vec![ -// Point::new(0, 21)..Point::new(0, 24), -// Point::new(3, 20)..Point::new(3, 22), -// ], -// true, -// cx, -// ); -// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); +// editor // }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), -// DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), -// DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), -// ] -// ); // } // #[gpui::test] -// async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { +// fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { // init_test(cx, |_| {}); -// let language = Arc::new( -// Language::new( -// LanguageConfig { -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: false, -// newline: true, -// }, -// BracketPair { -// start: "(".to_string(), -// end: ")".to_string(), -// close: false, -// newline: true, -// }, -// ], -// ..Default::default() -// }, -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query( -// r#" -// (_ "(" ")" @end) @indent -// (_ "{" "}" @end) @indent -// "#, -// ) -// .unwrap(), -// ); +// cx.add_window(|cx| { +// let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); +// let mut editor = build_editor(buffer.clone(), cx); +// let buffer = buffer.read(cx).as_singleton().unwrap(); -// let text = "fn a() {}"; +// editor.change_selections(None, cx, |s| { +// s.select_ranges([ +// Point::new(0, 2)..Point::new(1, 1), +// Point::new(1, 2)..Point::new(1, 2), +// Point::new(3, 1)..Point::new(3, 2), +// ]) +// }); -// let buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor -// .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) -// .await; +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n"); -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); -// editor.newline(&Newline, cx); -// assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); // assert_eq!( -// editor.selections.ranges(cx), -// &[ -// Point::new(1, 4)..Point::new(1, 4), -// Point::new(3, 4)..Point::new(3, 4), -// Point::new(5, 0)..Point::new(5, 0) +// editor.selections.ranges::(cx), +// [ +// Point::new(0, 7)..Point::new(0, 7), +// Point::new(1, 3)..Point::new(1, 3) // ] // ); +// editor // }); // } -// #[gpui::test] -// async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Test sort_lines_case_insensitive() + cx.set_state(indoc! {" + «z + y + x + Z + Y + Xˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx)); + cx.assert_editor_state(indoc! {" + «x + X + y + Y + z + Zˇ» + "}); + + // Test reverse_lines() + cx.set_state(indoc! {" + «5 + 4 + 3 + 2 + 1ˇ» + "}); + cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx)); + cx.assert_editor_state(indoc! {" + «1 + 2 + 3 + 4 + 5ˇ» + "}); + + // Skip testing shuffle_line() + + // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive() + // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines) + + // Don't manipulate when cursor is on single line, but expand the selection + cx.set_state(indoc! {" + ddˇdd + ccc + bb + a + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «ddddˇ» + ccc + bb + a + "}); + + // Basic manipulate case + // Start selection moves to column 0 + // End of selection shrinks to fit shorter line + cx.set_state(indoc! {" + dd«d + ccc + bb + aaaaaˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «aaaaa + bb + ccc + dddˇ» + "}); + + // Manipulate case with newlines + cx.set_state(indoc! {" + dd«d + ccc + + bb + aaaaa + + ˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + « + + aaaaa + bb + ccc + dddˇ» + + "}); +} -// let mut cx = EditorTestContext::new(cx).await; +#[gpui::test] +async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Manipulate with multiple selections on a single line + cx.set_state(indoc! {" + dd«dd + cˇ»c«c + bb + aaaˇ»aa + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «aaaaa + bb + ccc + ddddˇ» + "}); + + // Manipulate with multiple disjoin selections + cx.set_state(indoc! {" + 5« + 4 + 3 + 2 + 1ˇ» + + dd«dd + ccc + bb + aaaˇ»aa + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «1 + 2 + 3 + 4 + 5ˇ» + + «aaaaa + bb + ccc + ddddˇ» + "}); +} -// let language = Arc::new(Language::new( -// LanguageConfig { -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }, -// BracketPair { -// start: "(".to_string(), -// end: ")".to_string(), -// close: true, -// newline: true, -// }, -// BracketPair { -// start: "/*".to_string(), -// end: " */".to_string(), -// close: true, -// newline: true, -// }, -// BracketPair { -// start: "[".to_string(), -// end: "]".to_string(), -// close: false, -// newline: true, -// }, -// BracketPair { -// start: "\"".to_string(), -// end: "\"".to_string(), -// close: true, -// newline: false, -// }, -// ], -// ..Default::default() -// }, -// autoclose_before: "})]".to_string(), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )); +#[gpui::test] +async fn test_manipulate_text(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Test convert_to_upper_case() + cx.set_state(indoc! {" + «hello worldˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «HELLO WORLDˇ» + "}); + + // Test convert_to_lower_case() + cx.set_state(indoc! {" + «HELLO WORLDˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx)); + cx.assert_editor_state(indoc! {" + «hello worldˇ» + "}); + + // Test multiple line, single selection case + // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary + cx.set_state(indoc! {" + «The quick brown + fox jumps over + the lazy dogˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx)); + cx.assert_editor_state(indoc! {" + «The Quick Brown + Fox Jumps Over + The Lazy Dogˇ» + "}); + + // Test multiple line, single selection case + // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary + cx.set_state(indoc! {" + «The quick brown + fox jumps over + the lazy dogˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx)); + cx.assert_editor_state(indoc! {" + «TheQuickBrown + FoxJumpsOver + TheLazyDogˇ» + "}); + + // From here on out, test more complex cases of manipulate_text() + + // Test no selection case - should affect words cursors are in + // Cursor at beginning, middle, and end of word + cx.set_state(indoc! {" + ˇhello big beauˇtiful worldˇ + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ» + "}); + + // Test multiple selections on a single line and across multiple lines + cx.set_state(indoc! {" + «Theˇ» quick «brown + foxˇ» jumps «overˇ» + the «lazyˇ» dog + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «THEˇ» quick «BROWN + FOXˇ» jumps «OVERˇ» + the «LAZYˇ» dog + "}); + + // Test case where text length grows + cx.set_state(indoc! {" + «tschüߡ» + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «TSCHÜSSˇ» + "}); + + // Test to make sure we don't crash when text shrinks + cx.set_state(indoc! {" + aaa_bbbˇ + "}); + cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); + cx.assert_editor_state(indoc! {" + «aaaBbbˇ» + "}); + + // Test to make sure we all aware of the fact that each word can grow and shrink + // Final selections should be aware of this fact + cx.set_state(indoc! {" + aaa_bˇbb bbˇb_ccc ˇccc_ddd + "}); + cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); + cx.assert_editor_state(indoc! {" + «aaaBbbˇ» «bbbCccˇ» «cccDddˇ» + "}); +} -// let registry = Arc::new(LanguageRegistry::test()); -// registry.add(language.clone()); -// cx.update_buffer(|buffer, cx| { -// buffer.set_language_registry(registry); -// buffer.set_language(Some(language), cx); -// }); +#[gpui::test] +fn test_duplicate_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// cx.set_state( -// &r#" -// 🏀ˇ -// εˇ -// ❤️ˇ -// "# -// .unindent(), -// ); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ]) + }); + view.duplicate_line(&DuplicateLine, cx); + assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), + ] + ); + }); + + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), + ]) + }); + view.duplicate_line(&DuplicateLine, cx); + assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), + DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), + ] + ); + }); +} -// // autoclose multiple nested brackets at multiple cursors -// cx.update_editor(|view, cx| { -// view.handle_input("{", cx); -// view.handle_input("{", cx); -// view.handle_input("{", cx); -// }); -// cx.assert_editor_state( -// &" -// 🏀{{{ˇ}}} -// ε{{{ˇ}}} -// ❤️{{{ˇ}}} -// " -// .unindent(), -// ); +#[gpui::test] +fn test_move_line_up_down(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// // insert a different closing bracket -// cx.update_editor(|view, cx| { -// view.handle_input(")", cx); -// }); -// cx.assert_editor_state( -// &" -// 🏀{{{)ˇ}}} -// ε{{{)ˇ}}} -// ❤️{{{)ˇ}}} -// " -// .unindent(), -// ); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + build_editor(buffer, cx) + }); + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + true, + cx, + ); + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), + ]) + }); + assert_eq!( + view.display_text(cx), + "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj" + ); + + view.move_line_up(&MoveLineUp, cx); + assert_eq!( + view.display_text(cx), + "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff" + ); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); + }); -// // skip over the auto-closed brackets when typing a closing bracket -// cx.update_editor(|view, cx| { -// view.move_right(&MoveRight, cx); -// view.handle_input("}", cx); -// view.handle_input("}", cx); -// view.handle_input("}", cx); -// }); -// cx.assert_editor_state( -// &" -// 🏀{{{)}}}}ˇ -// ε{{{)}}}}ˇ -// ❤️{{{)}}}}ˇ -// " -// .unindent(), -// ); + view.update(cx, |view, cx| { + view.move_line_down(&MoveLineDown, cx); + assert_eq!( + view.display_text(cx), + "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj" + ); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + }); -// // autoclose multi-character pairs -// cx.set_state( -// &" -// ˇ -// ˇ -// " -// .unindent(), -// ); -// cx.update_editor(|view, cx| { -// view.handle_input("/", cx); -// view.handle_input("*", cx); -// }); -// cx.assert_editor_state( -// &" -// /*ˇ */ -// /*ˇ */ -// " -// .unindent(), -// ); + view.update(cx, |view, cx| { + view.move_line_down(&MoveLineDown, cx); + assert_eq!( + view.display_text(cx), + "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj" + ); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + }); -// // one cursor autocloses a multi-character pair, one cursor -// // does not autoclose. -// cx.set_state( -// &" -// /ˇ -// ˇ -// " -// .unindent(), -// ); -// cx.update_editor(|view, cx| view.handle_input("*", cx)); -// cx.assert_editor_state( -// &" -// /*ˇ */ -// *ˇ -// " -// .unindent(), -// ); + view.update(cx, |view, cx| { + view.move_line_up(&MoveLineUp, cx); + assert_eq!( + view.display_text(cx), + "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff" + ); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); + }); +} -// // Don't autoclose if the next character isn't whitespace and isn't -// // listed in the language's "autoclose_before" section. -// cx.set_state("ˇa b"); -// cx.update_editor(|view, cx| view.handle_input("{", cx)); -// cx.assert_editor_state("{ˇa b"); - -// // Don't autoclose if `close` is false for the bracket pair -// cx.set_state("ˇ"); -// cx.update_editor(|view, cx| view.handle_input("[", cx)); -// cx.assert_editor_state("[ˇ"); - -// // Surround with brackets if text is selected -// cx.set_state("«aˇ» b"); -// cx.update_editor(|view, cx| view.handle_input("{", cx)); -// cx.assert_editor_state("{«aˇ»} b"); - -// // Autclose pair where the start and end characters are the same -// cx.set_state("aˇ"); -// cx.update_editor(|view, cx| view.handle_input("\"", cx)); -// cx.assert_editor_state("a\"ˇ\""); -// cx.update_editor(|view, cx| view.handle_input("\"", cx)); -// cx.assert_editor_state("a\"\"ˇ"); -// } +#[gpui::test] +fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + build_editor(buffer, cx) + }); + editor.update(cx, |editor, cx| { + let snapshot = editor.buffer.read(cx).snapshot(cx); + editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Fixed, + position: snapshot.anchor_after(Point::new(2, 0)), + disposition: BlockDisposition::Below, + height: 1, + render: Arc::new(|_| div().render()), + }], + Some(Autoscroll::fit()), + cx, + ); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) + }); + editor.move_line_down(&MoveLineDown, cx); + }); +} +//todo!(test_transpose) // #[gpui::test] -// async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { +// fn test_transpose(cx: &mut TestAppContext) { // init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; +// _ = cx.add_window(|cx| { +// let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); -// let html_language = Arc::new( -// Language::new( -// LanguageConfig { -// name: "HTML".into(), -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "<".into(), -// end: ">".into(), -// close: true, -// ..Default::default() -// }, -// BracketPair { -// start: "{".into(), -// end: "}".into(), -// close: true, -// ..Default::default() -// }, -// BracketPair { -// start: "(".into(), -// end: ")".into(), -// close: true, -// ..Default::default() -// }, -// ], -// ..Default::default() -// }, -// autoclose_before: "})]>".into(), -// ..Default::default() -// }, -// Some(tree_sitter_html::language()), -// ) -// .with_injection_query( -// r#" -// (script_element -// (raw_text) @content -// (#set! "language" "javascript")) -// "#, -// ) -// .unwrap(), -// ); +// editor.change_selections(None, cx, |s| s.select_ranges([1..1])); +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "bac"); +// assert_eq!(editor.selections.ranges(cx), [2..2]); -// let javascript_language = Arc::new(Language::new( -// LanguageConfig { -// name: "JavaScript".into(), -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "/*".into(), -// end: " */".into(), -// close: true, -// ..Default::default() -// }, -// BracketPair { -// start: "{".into(), -// end: "}".into(), -// close: true, -// ..Default::default() -// }, -// BracketPair { -// start: "(".into(), -// end: ")".into(), -// close: true, -// ..Default::default() -// }, -// ], -// ..Default::default() -// }, -// autoclose_before: "})]>".into(), -// ..Default::default() -// }, -// Some(tree_sitter_typescript::language_tsx()), -// )); +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "bca"); +// assert_eq!(editor.selections.ranges(cx), [3..3]); -// let registry = Arc::new(LanguageRegistry::test()); -// registry.add(html_language.clone()); -// registry.add(javascript_language.clone()); +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "bac"); +// assert_eq!(editor.selections.ranges(cx), [3..3]); -// cx.update_buffer(|buffer, cx| { -// buffer.set_language_registry(registry); -// buffer.set_language(Some(html_language), cx); +// editor // }); -// cx.set_state( -// &r#" -// ˇ -// -// ˇ -// "# -// .unindent(), -// ); +// _ = cx.add_window(|cx| { +// let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); -// // Precondition: different languages are active at different locations. -// cx.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// let cursors = editor.selections.ranges::(cx); -// let languages = cursors -// .iter() -// .map(|c| snapshot.language_at(c.start).unwrap().name()) -// .collect::>(); -// assert_eq!( -// languages, -// &["HTML".into(), "JavaScript".into(), "HTML".into()] -// ); -// }); +// editor.change_selections(None, cx, |s| s.select_ranges([3..3])); +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "acb\nde"); +// assert_eq!(editor.selections.ranges(cx), [3..3]); -// // Angle brackets autoclose in HTML, but not JavaScript. -// cx.update_editor(|editor, cx| { -// editor.handle_input("<", cx); -// editor.handle_input("a", cx); -// }); -// cx.assert_editor_state( -// &r#" -// -// -// -// "# -// .unindent(), -// ); +// editor.change_selections(None, cx, |s| s.select_ranges([4..4])); +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "acbd\ne"); +// assert_eq!(editor.selections.ranges(cx), [5..5]); -// // Curly braces and parens autoclose in both HTML and JavaScript. -// cx.update_editor(|editor, cx| { -// editor.handle_input(" b=", cx); -// editor.handle_input("{", cx); -// editor.handle_input("c", cx); -// editor.handle_input("(", cx); -// }); -// cx.assert_editor_state( -// &r#" -// -// -// -// "# -// .unindent(), -// ); +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "acbde\n"); +// assert_eq!(editor.selections.ranges(cx), [6..6]); -// // Brackets that were already autoclosed are skipped. -// cx.update_editor(|editor, cx| { -// editor.handle_input(")", cx); -// editor.handle_input("d", cx); -// editor.handle_input("}", cx); -// }); -// cx.assert_editor_state( -// &r#" -// -// -// -// "# -// .unindent(), -// ); -// cx.update_editor(|editor, cx| { -// editor.handle_input(">", cx); +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "acbd\ne"); +// assert_eq!(editor.selections.ranges(cx), [6..6]); + +// editor // }); -// cx.assert_editor_state( -// &r#" -// ˇ -// -// ˇ -// "# -// .unindent(), -// ); -// // Reset -// cx.set_state( -// &r#" -// ˇ -// -// ˇ -// "# -// .unindent(), -// ); +// _ = cx.add_window(|cx| { +// let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); -// cx.update_editor(|editor, cx| { -// editor.handle_input("<", cx); -// }); -// cx.assert_editor_state( -// &r#" -// <ˇ> -// -// <ˇ> -// "# -// .unindent(), -// ); +// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "bacd\ne"); +// assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); -// // When backspacing, the closing angle brackets are removed. -// cx.update_editor(|editor, cx| { -// editor.backspace(&Backspace, cx); +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "bcade\n"); +// assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]); + +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "bcda\ne"); +// assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); + +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "bcade\n"); +// assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); + +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "bcaed\n"); +// assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); + +// editor // }); -// cx.assert_editor_state( -// &r#" -// ˇ -// -// ˇ -// "# -// .unindent(), -// ); -// // Block comments autoclose in JavaScript, but not HTML. -// cx.update_editor(|editor, cx| { -// editor.handle_input("/", cx); -// editor.handle_input("*", cx); +// _ = cx.add_window(|cx| { +// let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); + +// editor.change_selections(None, cx, |s| s.select_ranges([4..4])); +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "🏀🍐✋"); +// assert_eq!(editor.selections.ranges(cx), [8..8]); + +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "🏀✋🍐"); +// assert_eq!(editor.selections.ranges(cx), [11..11]); + +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "🏀🍐✋"); +// assert_eq!(editor.selections.ranges(cx), [11..11]); + +// editor // }); -// cx.assert_editor_state( -// &r#" -// /*ˇ -// -// /*ˇ -// "# -// .unindent(), -// ); // } +//todo!(clipboard) // #[gpui::test] -// async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { +// async fn test_clipboard(cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); // let mut cx = EditorTestContext::new(cx).await; -// let rust_language = Arc::new( -// Language::new( -// LanguageConfig { -// name: "Rust".into(), -// brackets: serde_json::from_value(json!([ -// { "start": "{", "end": "}", "close": true, "newline": true }, -// { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] }, -// ])) -// .unwrap(), -// autoclose_before: "})]>".into(), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_override_query("(string_literal) @string") -// .unwrap(), -// ); - -// let registry = Arc::new(LanguageRegistry::test()); -// registry.add(rust_language.clone()); - -// cx.update_buffer(|buffer, cx| { -// buffer.set_language_registry(registry); -// buffer.set_language(Some(rust_language), cx); -// }); +// cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); +// cx.update_editor(|e, cx| e.cut(&Cut, cx)); +// cx.assert_editor_state("ˇtwo ˇfour ˇsix "); -// cx.set_state( -// &r#" -// let x = ˇ -// "# -// .unindent(), -// ); +// // Paste with three cursors. Each cursor pastes one slice of the clipboard text. +// cx.set_state("two ˇfour ˇsix ˇ"); +// cx.update_editor(|e, cx| e.paste(&Paste, cx)); +// cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ"); -// // Inserting a quotation mark. A closing quotation mark is automatically inserted. -// cx.update_editor(|editor, cx| { -// editor.handle_input("\"", cx); +// // Paste again but with only two cursors. Since the number of cursors doesn't +// // match the number of slices in the clipboard, the entire clipboard text +// // is pasted at each cursor. +// cx.set_state("ˇtwo one✅ four three six five ˇ"); +// cx.update_editor(|e, cx| { +// e.handle_input("( ", cx); +// e.paste(&Paste, cx); +// e.handle_input(") ", cx); // }); // cx.assert_editor_state( -// &r#" -// let x = "ˇ" -// "# -// .unindent(), +// &([ +// "( one✅ ", +// "three ", +// "five ) ˇtwo one✅ four three six five ( one✅ ", +// "three ", +// "five ) ˇ", +// ] +// .join("\n")), // ); -// // Inserting another quotation mark. The cursor moves across the existing -// // automatically-inserted quotation mark. -// cx.update_editor(|editor, cx| { -// editor.handle_input("\"", cx); -// }); -// cx.assert_editor_state( -// &r#" -// let x = ""ˇ -// "# -// .unindent(), -// ); +// // Cut with three selections, one of which is full-line. +// cx.set_state(indoc! {" +// 1«2ˇ»3 +// 4ˇ567 +// «8ˇ»9"}); +// cx.update_editor(|e, cx| e.cut(&Cut, cx)); +// cx.assert_editor_state(indoc! {" +// 1ˇ3 +// ˇ9"}); -// // Reset -// cx.set_state( -// &r#" -// let x = ˇ -// "# -// .unindent(), -// ); +// // Paste with three selections, noticing how the copied selection that was full-line +// // gets inserted before the second cursor. +// cx.set_state(indoc! {" +// 1ˇ3 +// 9ˇ +// «oˇ»ne"}); +// cx.update_editor(|e, cx| e.paste(&Paste, cx)); +// cx.assert_editor_state(indoc! {" +// 12ˇ3 +// 4567 +// 9ˇ +// 8ˇne"}); -// // Inserting a quotation mark inside of a string. A second quotation mark is not inserted. -// cx.update_editor(|editor, cx| { -// editor.handle_input("\"", cx); -// editor.handle_input(" ", cx); -// editor.move_left(&Default::default(), cx); -// editor.handle_input("\\", cx); -// editor.handle_input("\"", cx); -// }); -// cx.assert_editor_state( -// &r#" -// let x = "\"ˇ " -// "# -// .unindent(), -// ); +// // Copy with a single cursor only, which writes the whole line into the clipboard. +// cx.set_state(indoc! {" +// The quick brown +// fox juˇmps over +// the lazy dog"}); +// cx.update_editor(|e, cx| e.copy(&Copy, cx)); +// cx.cx.assert_clipboard_content(Some("fox jumps over\n")); -// // Inserting a closing quotation mark at the position of an automatically-inserted quotation -// // mark. Nothing is inserted. -// cx.update_editor(|editor, cx| { -// editor.move_right(&Default::default(), cx); -// editor.handle_input("\"", cx); -// }); -// cx.assert_editor_state( -// &r#" -// let x = "\" "ˇ -// "# -// .unindent(), -// ); +// // Paste with three selections, noticing how the copied full-line selection is inserted +// // before the empty selections but replaces the selection that is non-empty. +// cx.set_state(indoc! {" +// Tˇhe quick brown +// «foˇ»x jumps over +// tˇhe lazy dog"}); +// cx.update_editor(|e, cx| e.paste(&Paste, cx)); +// cx.assert_editor_state(indoc! {" +// fox jumps over +// Tˇhe quick brown +// fox jumps over +// ˇx jumps over +// fox jumps over +// tˇhe lazy dog"}); // } // #[gpui::test] -// async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { +// async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); +// let mut cx = EditorTestContext::new(cx).await; // let language = Arc::new(Language::new( -// LanguageConfig { -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }, -// BracketPair { -// start: "/* ".to_string(), -// end: "*/".to_string(), -// close: true, -// ..Default::default() -// }, -// ], -// ..Default::default() -// }, -// ..Default::default() -// }, +// LanguageConfig::default(), // Some(tree_sitter_rust::language()), // )); +// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); -// let text = r#" -// a -// b -// c -// "# -// .unindent(); - -// let buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) -// .await; - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), -// ]) -// }); - -// view.handle_input("{", cx); -// view.handle_input("{", cx); -// view.handle_input("{", cx); -// assert_eq!( -// view.text(cx), -// " -// {{{a}}} -// {{{b}}} -// {{{c}}} -// " -// .unindent() +// // Cut an indented block, without the leading whitespace. +// cx.set_state(indoc! {" +// const a: B = ( +// c(), +// «d( +// e, +// f +// )ˇ» // ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4), -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4), -// DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4) -// ] +// "}); +// cx.update_editor(|e, cx| e.cut(&Cut, cx)); +// cx.assert_editor_state(indoc! {" +// const a: B = ( +// c(), +// ˇ // ); +// "}); -// view.undo(&Undo, cx); -// view.undo(&Undo, cx); -// view.undo(&Undo, cx); -// assert_eq!( -// view.text(cx), -// " -// a -// b -// c -// " -// .unindent() -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) -// ] +// // Paste it at the same position. +// cx.update_editor(|e, cx| e.paste(&Paste, cx)); +// cx.assert_editor_state(indoc! {" +// const a: B = ( +// c(), +// d( +// e, +// f +// )ˇ // ); +// "}); -// // Ensure inserting the first character of a multi-byte bracket pair -// // doesn't surround the selections with the bracket. -// view.handle_input("/", cx); -// assert_eq!( -// view.text(cx), -// " -// / -// / -// / -// " -// .unindent() +// // Paste it at a line with a lower indent level. +// cx.set_state(indoc! {" +// ˇ +// const a: B = ( +// c(), // ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) -// ] +// "}); +// cx.update_editor(|e, cx| e.paste(&Paste, cx)); +// cx.assert_editor_state(indoc! {" +// d( +// e, +// f +// )ˇ +// const a: B = ( +// c(), // ); +// "}); -// view.undo(&Undo, cx); -// assert_eq!( -// view.text(cx), -// " -// a -// b -// c -// " -// .unindent() -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) -// ] -// ); +// // Cut an indented block, with the leading whitespace. +// cx.set_state(indoc! {" +// const a: B = ( +// c(), +// « d( +// e, +// f +// ) +// ˇ»); +// "}); +// cx.update_editor(|e, cx| e.cut(&Cut, cx)); +// cx.assert_editor_state(indoc! {" +// const a: B = ( +// c(), +// ˇ); +// "}); -// // Ensure inserting the last character of a multi-byte bracket pair -// // doesn't surround the selections with the bracket. -// view.handle_input("*", cx); -// assert_eq!( -// view.text(cx), -// " -// * -// * -// * -// " -// .unindent() +// // Paste it at the same position. +// cx.update_editor(|e, cx| e.paste(&Paste, cx)); +// cx.assert_editor_state(indoc! {" +// const a: B = ( +// c(), +// d( +// e, +// f +// ) +// ˇ); +// "}); + +// // Paste it at a line with a higher indent level. +// cx.set_state(indoc! {" +// const a: B = ( +// c(), +// d( +// e, +// fˇ +// ) // ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) -// ] +// "}); +// cx.update_editor(|e, cx| e.paste(&Paste, cx)); +// cx.assert_editor_state(indoc! {" +// const a: B = ( +// c(), +// d( +// e, +// f d( +// e, +// f +// ) +// ˇ +// ) // ); -// }); +// "}); // } -// #[gpui::test] -// async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +fn test_select_all(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// let language = Arc::new(Language::new( -// LanguageConfig { -// brackets: BracketPairConfig { -// pairs: vec![BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }], -// ..Default::default() -// }, -// autoclose_before: "}".to_string(), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); + build_editor(buffer, cx) + }); + view.update(cx, |view, cx| { + view.select_all(&SelectAll, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] + ); + }); +} -// let text = r#" -// a -// b -// c -// "# -// .unindent(); - -// let buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor -// .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) -// .await; +#[gpui::test] +fn test_select_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([ -// Point::new(0, 1)..Point::new(0, 1), -// Point::new(1, 1)..Point::new(1, 1), -// Point::new(2, 1)..Point::new(2, 1), -// ]) -// }); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); + build_editor(buffer, cx) + }); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), + ]) + }); + view.select_line(&SelectLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), + ] + ); + }); -// editor.handle_input("{", cx); -// editor.handle_input("{", cx); -// editor.handle_input("_", cx); -// assert_eq!( -// editor.text(cx), -// " -// a{{_}} -// b{{_}} -// c{{_}} -// " -// .unindent() -// ); -// assert_eq!( -// editor.selections.ranges::(cx), -// [ -// Point::new(0, 4)..Point::new(0, 4), -// Point::new(1, 4)..Point::new(1, 4), -// Point::new(2, 4)..Point::new(2, 4) -// ] -// ); + view.update(cx, |view, cx| { + view.select_line(&SelectLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), + ] + ); + }); -// editor.backspace(&Default::default(), cx); -// editor.backspace(&Default::default(), cx); -// assert_eq!( -// editor.text(cx), -// " -// a{} -// b{} -// c{} -// " -// .unindent() -// ); -// assert_eq!( -// editor.selections.ranges::(cx), -// [ -// Point::new(0, 2)..Point::new(0, 2), -// Point::new(1, 2)..Point::new(1, 2), -// Point::new(2, 2)..Point::new(2, 2) -// ] -// ); + view.update(cx, |view, cx| { + view.select_line(&SelectLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] + ); + }); +} -// editor.delete_to_previous_word_start(&Default::default(), cx); -// assert_eq!( -// editor.text(cx), -// " -// a -// b -// c -// " -// .unindent() -// ); -// assert_eq!( -// editor.selections.ranges::(cx), -// [ -// Point::new(0, 1)..Point::new(0, 1), -// Point::new(1, 1)..Point::new(1, 1), -// Point::new(2, 1)..Point::new(2, 1) -// ] -// ); -// }); -// } +#[gpui::test] +fn test_split_selection_into_lines(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); + build_editor(buffer, cx) + }); + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + true, + cx, + ); + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ]) + }); + assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"); + }); + + view.update(cx, |view, cx| { + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) + ] + ); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)]) + }); + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), + DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), + DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), + DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), + DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) + ] + ); + }); +} + +#[gpui::test] +async fn test_add_selection_above_below(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); + cx.set_state(indoc!( + r#"abc + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|editor, cx| { + editor.add_selection_above(&Default::default(), cx); + }); -// #[gpui::test] -// async fn test_snippets(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + cx.assert_editor_state(indoc!( + r#"abcˇ + defˇghi -// let (text, insertion_ranges) = marked_text_ranges( -// indoc! {" -// a.ˇ b -// a.ˇ b -// a.ˇ b -// "}, -// false, -// ); + jk + nlmo + "# + )); -// let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + cx.update_editor(|editor, cx| { + editor.add_selection_above(&Default::default(), cx); + }); -// editor.update(cx, |editor, cx| { -// let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); + cx.assert_editor_state(indoc!( + r#"abcˇ + defˇghi -// editor -// .insert_snippet(&insertion_ranges, snippet, cx) -// .unwrap(); + jk + nlmo + "# + )); -// fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { -// let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); -// assert_eq!(editor.text(cx), expected_text); -// assert_eq!(editor.selections.ranges::(cx), selection_ranges); -// } + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); + }); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// "}, -// ); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi -// // Can't move earlier than the first tab stop -// assert!(!editor.move_to_prev_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// "}, -// ); + jk + nlmo + "# + )); -// assert!(editor.move_to_next_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// "}, -// ); + cx.update_editor(|view, cx| { + view.undo_selection(&Default::default(), cx); + }); -// editor.move_to_prev_snippet_tabstop(cx); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// "}, -// ); + cx.assert_editor_state(indoc!( + r#"abcˇ + defˇghi -// assert!(editor.move_to_next_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// "}, -// ); -// assert!(editor.move_to_next_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// "}, -// ); + jk + nlmo + "# + )); -// // As soon as the last tab stop is reached, snippet state is gone -// editor.move_to_prev_snippet_tabstop(cx); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// "}, -// ); -// }); -// } + cx.update_editor(|view, cx| { + view.redo_selection(&Default::default(), cx); + }); -// #[gpui::test] -// async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; + jk + nlmo + "# + )); -// let fs = FakeFs::new(cx.background()); -// fs.insert_file("/file.rs", Default::default()).await; + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); + }); -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) -// .await -// .unwrap(); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); + jk + nlmˇo + "# + )); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); -// assert!(cx.read(|cx| editor.is_dirty(cx))); + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); + }); -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// fake_server -// .handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// assert_eq!(params.options.tab_size, 4); -// Ok(Some(vec![lsp::TextEdit::new( -// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), -// ", ".to_string(), -// )])) -// }) -// .next() -// .await; -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one, two\nthree\n" -// ); -// assert!(!cx.read(|cx| editor.is_dirty(cx))); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); -// assert!(cx.read(|cx| editor.is_dirty(cx))); + jk + nlmˇo + "# + )); -// // Ensure we can still save even if formatting hangs. -// fake_server.handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// futures::future::pending::<()>().await; -// unreachable!() -// }); -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// cx.foreground().advance_clock(super::FORMAT_TIMEOUT); -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one\ntwo\nthree\n" -// ); -// assert!(!cx.read(|cx| editor.is_dirty(cx))); - -// // Set rust language override and assert overridden tabsize is sent to language server -// update_test_language_settings(cx, |settings| { -// settings.languages.insert( -// "Rust".into(), -// LanguageSettingsContent { -// tab_size: NonZeroU32::new(8), -// ..Default::default() -// }, -// ); -// }); + // change selections + cx.set_state(indoc!( + r#"abc + def«ˇg»hi -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// fake_server -// .handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// assert_eq!(params.options.tab_size, 8); -// Ok(Some(vec![])) -// }) -// .next() -// .await; -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// } + jk + nlmo + "# + )); -// #[gpui::test] -// async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); + }); -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_range_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi -// let fs = FakeFs::new(cx.background()); -// fs.insert_file("/file.rs", Default::default()).await; + jk + nlm«ˇo» + "# + )); -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) -// .await -// .unwrap(); + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); + }); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); -// assert!(cx.read(|cx| editor.is_dirty(cx))); + jk + nlm«ˇo» + "# + )); -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// fake_server -// .handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// assert_eq!(params.options.tab_size, 4); -// Ok(Some(vec![lsp::TextEdit::new( -// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), -// ", ".to_string(), -// )])) -// }) -// .next() -// .await; -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one, two\nthree\n" -// ); -// assert!(!cx.read(|cx| editor.is_dirty(cx))); + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); + }); -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); -// assert!(cx.read(|cx| editor.is_dirty(cx))); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi -// // Ensure we can still save even if formatting hangs. -// fake_server.handle_request::( -// move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// futures::future::pending::<()>().await; -// unreachable!() -// }, -// ); -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// cx.foreground().advance_clock(super::FORMAT_TIMEOUT); -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one\ntwo\nthree\n" -// ); -// assert!(!cx.read(|cx| editor.is_dirty(cx))); - -// // Set rust language override and assert overridden tabsize is sent to language server -// update_test_language_settings(cx, |settings| { -// settings.languages.insert( -// "Rust".into(), -// LanguageSettingsContent { -// tab_size: NonZeroU32::new(8), -// ..Default::default() -// }, -// ); -// }); + jk + nlmo + "# + )); -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// fake_server -// .handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// assert_eq!(params.options.tab_size, 8); -// Ok(Some(vec![])) -// }) -// .next() -// .await; -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// } + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); + }); -// #[gpui::test] -// async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) -// }); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// // Enable Prettier formatting for the same buffer, and ensure -// // LSP is called instead of Prettier. -// prettier_parser_name: Some("test_parser".to_string()), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; + jk + nlmo + "# + )); -// let fs = FakeFs::new(cx.background()); -// fs.insert_file("/file.rs", Default::default()).await; + // Change selections again + cx.set_state(indoc!( + r#"a«bc + defgˇ»hi -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// project.update(cx, |project, _| { -// project.languages().add(Arc::new(language)); -// }); -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) -// .await -// .unwrap(); + jk + nlmo + "# + )); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); + }); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + cx.assert_editor_state(indoc!( + r#"a«bcˇ» + d«efgˇ»hi -// let format = editor.update(cx, |editor, cx| { -// editor.perform_format(project.clone(), FormatTrigger::Manual, cx) -// }); -// fake_server -// .handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// assert_eq!(params.options.tab_size, 4); -// Ok(Some(vec![lsp::TextEdit::new( -// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), -// ", ".to_string(), -// )])) -// }) -// .next() -// .await; -// cx.foreground().start_waiting(); -// format.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one, two\nthree\n" -// ); + j«kˇ» + nlmo + "# + )); -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); -// // Ensure we don't lock if formatting hangs. -// fake_server.handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// futures::future::pending::<()>().await; -// unreachable!() -// }); -// let format = editor.update(cx, |editor, cx| { -// editor.perform_format(project, FormatTrigger::Manual, cx) -// }); -// cx.foreground().advance_clock(super::FORMAT_TIMEOUT); -// cx.foreground().start_waiting(); -// format.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one\ntwo\nthree\n" -// ); -// } + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); + }); + cx.assert_editor_state(indoc!( + r#"a«bcˇ» + d«efgˇ»hi + + j«kˇ» + n«lmoˇ» + "# + )); + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); + }); -// #[gpui::test] -// async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + cx.assert_editor_state(indoc!( + r#"a«bcˇ» + d«efgˇ»hi -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// document_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; + j«kˇ» + nlmo + "# + )); -// cx.set_state(indoc! {" -// one.twoˇ -// "}); + // Change selections again + cx.set_state(indoc!( + r#"abc + d«ˇefghi -// // The format request takes a long time. When it completes, it inserts -// // a newline and an indent before the `.` -// cx.lsp -// .handle_request::(move |_, cx| { -// let executor = cx.background(); -// async move { -// executor.timer(Duration::from_millis(100)).await; -// Ok(Some(vec![lsp::TextEdit { -// range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)), -// new_text: "\n ".into(), -// }])) -// } -// }); + jk + nlm»o + "# + )); -// // Submit a format request. -// let format_1 = cx -// .update_editor(|editor, cx| editor.format(&Format, cx)) -// .unwrap(); -// cx.foreground().run_until_parked(); + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); + }); -// // Submit a second format request. -// let format_2 = cx -// .update_editor(|editor, cx| editor.format(&Format, cx)) -// .unwrap(); -// cx.foreground().run_until_parked(); + cx.assert_editor_state(indoc!( + r#"a«ˇbc» + d«ˇef»ghi -// // Wait for both format requests to complete -// cx.foreground().advance_clock(Duration::from_millis(200)); -// cx.foreground().start_waiting(); -// format_1.await.unwrap(); -// cx.foreground().start_waiting(); -// format_2.await.unwrap(); + j«ˇk» + n«ˇlm»o + "# + )); -// // The formatting edits only happens once. -// cx.assert_editor_state(indoc! {" -// one -// .twoˇ -// "}); -// } + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); + }); -// #[gpui::test] -// async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.formatter = Some(language_settings::Formatter::Auto) -// }); + cx.assert_editor_state(indoc!( + r#"abc + d«ˇef»ghi -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// document_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; + j«ˇk» + n«ˇlm»o + "# + )); +} -// // Set up a buffer white some trailing whitespace and no trailing newline. -// cx.set_state( -// &[ -// "one ", // -// "twoˇ", // -// "three ", // -// "four", // -// ] -// .join("\n"), -// ); +#[gpui::test] +async fn test_select_next(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// // Submit a format request. -// let format = cx -// .update_editor(|editor, cx| editor.format(&Format, cx)) -// .unwrap(); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); -// // Record which buffer changes have been sent to the language server -// let buffer_changes = Arc::new(Mutex::new(Vec::new())); -// cx.lsp -// .handle_notification::({ -// let buffer_changes = buffer_changes.clone(); -// move |params, _| { -// buffer_changes.lock().extend( -// params -// .content_changes -// .into_iter() -// .map(|e| (e.range.unwrap(), e.text)), -// ); -// } -// }); + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); -// // Handle formatting requests to the language server. -// cx.lsp.handle_request::({ -// let buffer_changes = buffer_changes.clone(); -// move |_, _| { -// // When formatting is requested, trailing whitespace has already been stripped, -// // and the trailing newline has already been added. -// assert_eq!( -// &buffer_changes.lock()[1..], -// &[ -// ( -// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)), -// "".into() -// ), -// ( -// lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), -// "".into() -// ), -// ( -// lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)), -// "\n".into() -// ), -// ] -// ); + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); -// // Insert blank lines between each line of the buffer. -// async move { -// Ok(Some(vec![ -// lsp::TextEdit { -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), -// new_text: "\n".into(), -// }, -// lsp::TextEdit { -// range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)), -// new_text: "\n".into(), -// }, -// ])) -// } -// } -// }); + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); +} + +#[gpui::test] +async fn test_select_previous(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + { + // `Select previous` without a selection (selects wordwise) + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); + } + { + // `Select previous` with a selection + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); + } +} -// // After formatting the buffer, the trailing whitespace is stripped, -// // a newline is appended, and the edits provided by the language server -// // have been applied. -// format.await.unwrap(); -// cx.assert_editor_state( -// &[ -// "one", // -// "", // -// "twoˇ", // -// "", // -// "three", // -// "four", // -// "", // -// ] -// .join("\n"), -// ); +#[gpui::test] +async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// // Undoing the formatting undoes the trailing whitespace removal, the -// // trailing newline, and the LSP edits. -// cx.update_buffer(|buffer, cx| buffer.undo(cx)); -// cx.assert_editor_state( -// &[ -// "one ", // -// "twoˇ", // -// "three ", // -// "four", // -// ] -// .join("\n"), -// ); -// } + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + )); -// #[gpui::test] -// async fn test_completion(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + let text = r#" + use mod1::mod2::{mod3, mod4}; -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string(), ":".to_string()]), -// resolve_provider: Some(true), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; + fn fn_1(param1: bool, param2: &str) { + let var1 = "text"; + } + "# + .unindent(); -// cx.set_state(indoc! {" -// oneˇ -// two -// three -// "}); -// cx.simulate_keystroke("."); -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.|<> -// two -// three -// "}, -// vec!["first_completion", "second_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor.context_menu_next(&Default::default(), cx); -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {" -// one.second_completionˇ -// two -// three -// "}); + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + + view.condition::(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(&mut cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ]); + }); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| { view.selections.display_ranges(cx) }), + &[ + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] + ); + + // Trying to expand the selected syntax node one more time has no effect. + view.update(&mut cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] + ); -// handle_resolve_completion_request( -// &mut cx, -// Some(vec![ -// ( -// //This overlaps with the primary completion edit which is -// //misbehavior from the LSP spec, test that we filter it out -// indoc! {" -// one.second_ˇcompletion -// two -// threeˇ -// "}, -// "overlapping additional edit", -// ), -// ( -// indoc! {" -// one.second_completion -// two -// threeˇ -// "}, -// "\nadditional edit", -// ), -// ]), -// ) -// .await; -// apply_additional_edits.await.unwrap(); -// cx.assert_editor_state(indoc! {" -// one.second_completionˇ -// two -// three -// additional edit -// "}); + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ] + ); + + // Trying to shrink the selected syntax node one more time has no effect. + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ] + ); + + // Ensure that we keep expanding the selection if the larger selection starts or ends within + // a fold. + view.update(&mut cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 21)..Point::new(0, 24), + Point::new(3, 20)..Point::new(3, 22), + ], + true, + cx, + ); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), + ] + ); +} -// cx.set_state(indoc! {" -// one.second_completion -// twoˇ -// threeˇ -// additional edit -// "}); -// cx.simulate_keystroke(" "); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.simulate_keystroke("s"); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); +#[gpui::test] +async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let text = "fn a() {}"; + + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); + editor.newline(&Newline, cx); + assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(1, 4)..Point::new(1, 4), + Point::new(3, 4)..Point::new(3, 4), + Point::new(5, 0)..Point::new(5, 0) + ] + ); + }); +} -// cx.assert_editor_state(indoc! {" -// one.second_completion -// two sˇ -// three sˇ -// additional edit -// "}); -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.second_completion -// two s -// three -// additional edit -// "}, -// vec!["fourth_completion", "fifth_completion", "sixth_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; +#[gpui::test] +async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/*".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "[".to_string(), + end: "]".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "\"".to_string(), + end: "\"".to_string(), + close: true, + newline: false, + }, + ], + ..Default::default() + }, + autoclose_before: "})]".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(language.clone()); + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(language), cx); + }); -// cx.simulate_keystroke("i"); + cx.set_state( + &r#" + 🏀ˇ + εˇ + ❤️ˇ + "# + .unindent(), + ); + + // autoclose multiple nested brackets at multiple cursors + cx.update_editor(|view, cx| { + view.handle_input("{", cx); + view.handle_input("{", cx); + view.handle_input("{", cx); + }); + cx.assert_editor_state( + &" + 🏀{{{ˇ}}} + ε{{{ˇ}}} + ❤️{{{ˇ}}} + " + .unindent(), + ); + + // insert a different closing bracket + cx.update_editor(|view, cx| { + view.handle_input(")", cx); + }); + cx.assert_editor_state( + &" + 🏀{{{)ˇ}}} + ε{{{)ˇ}}} + ❤️{{{)ˇ}}} + " + .unindent(), + ); + + // skip over the auto-closed brackets when typing a closing bracket + cx.update_editor(|view, cx| { + view.move_right(&MoveRight, cx); + view.handle_input("}", cx); + view.handle_input("}", cx); + view.handle_input("}", cx); + }); + cx.assert_editor_state( + &" + 🏀{{{)}}}}ˇ + ε{{{)}}}}ˇ + ❤️{{{)}}}}ˇ + " + .unindent(), + ); + + // autoclose multi-character pairs + cx.set_state( + &" + ˇ + ˇ + " + .unindent(), + ); + cx.update_editor(|view, cx| { + view.handle_input("/", cx); + view.handle_input("*", cx); + }); + cx.assert_editor_state( + &" + /*ˇ */ + /*ˇ */ + " + .unindent(), + ); + + // one cursor autocloses a multi-character pair, one cursor + // does not autoclose. + cx.set_state( + &" + /ˇ + ˇ + " + .unindent(), + ); + cx.update_editor(|view, cx| view.handle_input("*", cx)); + cx.assert_editor_state( + &" + /*ˇ */ + *ˇ + " + .unindent(), + ); + + // Don't autoclose if the next character isn't whitespace and isn't + // listed in the language's "autoclose_before" section. + cx.set_state("ˇa b"); + cx.update_editor(|view, cx| view.handle_input("{", cx)); + cx.assert_editor_state("{ˇa b"); + + // Don't autoclose if `close` is false for the bracket pair + cx.set_state("ˇ"); + cx.update_editor(|view, cx| view.handle_input("[", cx)); + cx.assert_editor_state("[ˇ"); + + // Surround with brackets if text is selected + cx.set_state("«aˇ» b"); + cx.update_editor(|view, cx| view.handle_input("{", cx)); + cx.assert_editor_state("{«aˇ»} b"); + + // Autclose pair where the start and end characters are the same + cx.set_state("aˇ"); + cx.update_editor(|view, cx| view.handle_input("\"", cx)); + cx.assert_editor_state("a\"ˇ\""); + cx.update_editor(|view, cx| view.handle_input("\"", cx)); + cx.assert_editor_state("a\"\"ˇ"); +} -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.second_completion -// two si -// three -// additional edit -// "}, -// vec!["fourth_completion", "fifth_completion", "sixth_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; +#[gpui::test] +async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let html_language = Arc::new( + Language::new( + LanguageConfig { + name: "HTML".into(), + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "<".into(), + end: ">".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "{".into(), + end: "}".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "(".into(), + end: ")".into(), + close: true, + ..Default::default() + }, + ], + ..Default::default() + }, + autoclose_before: "})]>".into(), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_injection_query( + r#" + (script_element + (raw_text) @content + (#set! "language" "javascript")) + "#, + ) + .unwrap(), + ); + + let javascript_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "/*".into(), + end: " */".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "{".into(), + end: "}".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "(".into(), + end: ")".into(), + close: true, + ..Default::default() + }, + ], + ..Default::default() + }, + autoclose_before: "})]>".into(), + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(html_language.clone()); + registry.add(javascript_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(html_language), cx); + }); -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {" -// one.second_completion -// two sixth_completionˇ -// three sixth_completionˇ -// additional edit -// "}); + cx.set_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + // Precondition: different languages are active at different locations. + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let cursors = editor.selections.ranges::(cx); + let languages = cursors + .iter() + .map(|c| snapshot.language_at(c.start).unwrap().name()) + .collect::>(); + assert_eq!( + languages, + &["HTML".into(), "JavaScript".into(), "HTML".into()] + ); + }); -// handle_resolve_completion_request(&mut cx, None).await; -// apply_additional_edits.await.unwrap(); + // Angle brackets autoclose in HTML, but not JavaScript. + cx.update_editor(|editor, cx| { + editor.handle_input("<", cx); + editor.handle_input("a", cx); + }); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + + // Curly braces and parens autoclose in both HTML and JavaScript. + cx.update_editor(|editor, cx| { + editor.handle_input(" b=", cx); + editor.handle_input("{", cx); + editor.handle_input("c", cx); + editor.handle_input("(", cx); + }); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + + // Brackets that were already autoclosed are skipped. + cx.update_editor(|editor, cx| { + editor.handle_input(")", cx); + editor.handle_input("d", cx); + editor.handle_input("}", cx); + }); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + editor.handle_input(">", cx); + }); + cx.assert_editor_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + // Reset + cx.set_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + editor.handle_input("<", cx); + }); + cx.assert_editor_state( + &r#" + <ˇ> + + <ˇ> + "# + .unindent(), + ); + + // When backspacing, the closing angle brackets are removed. + cx.update_editor(|editor, cx| { + editor.backspace(&Backspace, cx); + }); + cx.assert_editor_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + // Block comments autoclose in JavaScript, but not HTML. + cx.update_editor(|editor, cx| { + editor.handle_input("/", cx); + editor.handle_input("*", cx); + }); + cx.assert_editor_state( + &r#" + /*ˇ + + /*ˇ + "# + .unindent(), + ); +} -// cx.update(|cx| { -// cx.update_global::(|settings, cx| { -// settings.update_user_settings::(cx, |settings| { -// settings.show_completions_on_input = Some(false); -// }); -// }) -// }); -// cx.set_state("editorˇ"); -// cx.simulate_keystroke("."); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.simulate_keystroke("c"); -// cx.simulate_keystroke("l"); -// cx.simulate_keystroke("o"); -// cx.assert_editor_state("editor.cloˇ"); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.update_editor(|editor, cx| { -// editor.show_completions(&ShowCompletions, cx); -// }); -// handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state("editor.closeˇ"); -// handle_resolve_completion_request(&mut cx, None).await; -// apply_additional_edits.await.unwrap(); -// } +#[gpui::test] +async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let rust_language = Arc::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + brackets: serde_json::from_value(json!([ + { "start": "{", "end": "}", "close": true, "newline": true }, + { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] }, + ])) + .unwrap(), + autoclose_before: "})]>".into(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_override_query("(string_literal) @string") + .unwrap(), + ); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(rust_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(rust_language), cx); + }); -// #[gpui::test] -// async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; -// let language = Arc::new(Language::new( -// LanguageConfig { -// line_comment: Some("// ".into()), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )); -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state( + &r#" + let x = ˇ + "# + .unindent(), + ); -// // If multiple selections intersect a line, the line is only toggled once. -// cx.set_state(indoc! {" -// fn a() { -// «//b(); -// ˇ»// «c(); -// //ˇ» d(); -// } -// "}); + // Inserting a quotation mark. A closing quotation mark is automatically inserted. + cx.update_editor(|editor, cx| { + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = "ˇ" + "# + .unindent(), + ); + + // Inserting another quotation mark. The cursor moves across the existing + // automatically-inserted quotation mark. + cx.update_editor(|editor, cx| { + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = ""ˇ + "# + .unindent(), + ); + + // Reset + cx.set_state( + &r#" + let x = ˇ + "# + .unindent(), + ); + + // Inserting a quotation mark inside of a string. A second quotation mark is not inserted. + cx.update_editor(|editor, cx| { + editor.handle_input("\"", cx); + editor.handle_input(" ", cx); + editor.move_left(&Default::default(), cx); + editor.handle_input("\\", cx); + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = "\"ˇ " + "# + .unindent(), + ); + + // Inserting a closing quotation mark at the position of an automatically-inserted quotation + // mark. Nothing is inserted. + cx.update_editor(|editor, cx| { + editor.move_right(&Default::default(), cx); + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = "\" "ˇ + "# + .unindent(), + ); +} -// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); +#[gpui::test] +async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/* ".to_string(), + end: "*/".to_string(), + close: true, + ..Default::default() + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = r#" + a + b + c + "# + .unindent(); + + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; + view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), + ]) + }); -// cx.assert_editor_state(indoc! {" -// fn a() { -// «b(); -// c(); -// ˇ» d(); -// } -// "}); + view.handle_input("{", cx); + view.handle_input("{", cx); + view.handle_input("{", cx); + assert_eq!( + view.text(cx), + " + {{{a}}} + {{{b}}} + {{{c}}} + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4) + ] + ); + + view.undo(&Undo, cx); + view.undo(&Undo, cx); + view.undo(&Undo, cx); + assert_eq!( + view.text(cx), + " + a + b + c + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) + ] + ); + + // Ensure inserting the first character of a multi-byte bracket pair + // doesn't surround the selections with the bracket. + view.handle_input("/", cx); + assert_eq!( + view.text(cx), + " + / + / + / + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) + ] + ); + + view.undo(&Undo, cx); + assert_eq!( + view.text(cx), + " + a + b + c + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) + ] + ); + + // Ensure inserting the last character of a multi-byte bracket pair + // doesn't surround the selections with the bracket. + view.handle_input("*", cx); + assert_eq!( + view.text(cx), + " + * + * + * + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) + ] + ); + }); +} -// // The comment prefix is inserted at the same column for every line in a -// // selection. -// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); +#[gpui::test] +async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }], + ..Default::default() + }, + autoclose_before: "}".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = r#" + a + b + c + "# + .unindent(); + + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; + editor + .condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + Point::new(2, 1)..Point::new(2, 1), + ]) + }); -// cx.assert_editor_state(indoc! {" -// fn a() { -// // «b(); -// // c(); -// ˇ»// d(); -// } -// "}); + editor.handle_input("{", cx); + editor.handle_input("{", cx); + editor.handle_input("_", cx); + assert_eq!( + editor.text(cx), + " + a{{_}} + b{{_}} + c{{_}} + " + .unindent() + ); + assert_eq!( + editor.selections.ranges::(cx), + [ + Point::new(0, 4)..Point::new(0, 4), + Point::new(1, 4)..Point::new(1, 4), + Point::new(2, 4)..Point::new(2, 4) + ] + ); + + editor.backspace(&Default::default(), cx); + editor.backspace(&Default::default(), cx); + assert_eq!( + editor.text(cx), + " + a{} + b{} + c{} + " + .unindent() + ); + assert_eq!( + editor.selections.ranges::(cx), + [ + Point::new(0, 2)..Point::new(0, 2), + Point::new(1, 2)..Point::new(1, 2), + Point::new(2, 2)..Point::new(2, 2) + ] + ); + + editor.delete_to_previous_word_start(&Default::default(), cx); + assert_eq!( + editor.text(cx), + " + a + b + c + " + .unindent() + ); + assert_eq!( + editor.selections.ranges::(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + Point::new(2, 1)..Point::new(2, 1) + ] + ); + }); +} -// // If a selection ends at the beginning of a line, that line is not toggled. -// cx.set_selections_state(indoc! {" -// fn a() { -// // b(); -// «// c(); -// ˇ» // d(); -// } -// "}); +// todo!(select_anchor_ranges) +// #[gpui::test] +// async fn test_snippets(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); -// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); +// let (text, insertion_ranges) = marked_text_ranges( +// indoc! {" +// a.ˇ b +// a.ˇ b +// a.ˇ b +// "}, +// false, +// ); -// cx.assert_editor_state(indoc! {" -// fn a() { -// // b(); -// «c(); -// ˇ» // d(); -// } -// "}); +// let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); +// let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); +// let cx = &mut cx; -// // If a selection span a single line and is empty, the line is toggled. -// cx.set_state(indoc! {" -// fn a() { -// a(); -// b(); -// ˇ -// } -// "}); +// editor.update(cx, |editor, cx| { +// let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); -// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); +// editor +// .insert_snippet(&insertion_ranges, snippet, cx) +// .unwrap(); -// cx.assert_editor_state(indoc! {" -// fn a() { -// a(); -// b(); -// //•ˇ +// fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { +// let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); +// assert_eq!(editor.text(cx), expected_text); +// assert_eq!(editor.selections.ranges::(cx), selection_ranges); // } -// "}); -// // If a selection span multiple lines, empty lines are not toggled. -// cx.set_state(indoc! {" -// fn a() { -// «a(); +// assert( +// editor, +// cx, +// indoc! {" +// a.f(«one», two, «three») b +// a.f(«one», two, «three») b +// a.f(«one», two, «three») b +// "}, +// ); -// c();ˇ» -// } -// "}); +// // Can't move earlier than the first tab stop +// assert!(!editor.move_to_prev_snippet_tabstop(cx)); +// assert( +// editor, +// cx, +// indoc! {" +// a.f(«one», two, «three») b +// a.f(«one», two, «three») b +// a.f(«one», two, «three») b +// "}, +// ); -// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); +// assert!(editor.move_to_next_snippet_tabstop(cx)); +// assert( +// editor, +// cx, +// indoc! {" +// a.f(one, «two», three) b +// a.f(one, «two», three) b +// a.f(one, «two», three) b +// "}, +// ); -// cx.assert_editor_state(indoc! {" -// fn a() { -// // «a(); +// editor.move_to_prev_snippet_tabstop(cx); +// assert( +// editor, +// cx, +// indoc! {" +// a.f(«one», two, «three») b +// a.f(«one», two, «three») b +// a.f(«one», two, «three») b +// "}, +// ); -// // c();ˇ» -// } -// "}); +// assert!(editor.move_to_next_snippet_tabstop(cx)); +// assert( +// editor, +// cx, +// indoc! {" +// a.f(one, «two», three) b +// a.f(one, «two», three) b +// a.f(one, «two», three) b +// "}, +// ); +// assert!(editor.move_to_next_snippet_tabstop(cx)); +// assert( +// editor, +// cx, +// indoc! {" +// a.f(one, two, three)ˇ b +// a.f(one, two, three)ˇ b +// a.f(one, two, three)ˇ b +// "}, +// ); + +// // As soon as the last tab stop is reached, snippet state is gone +// editor.move_to_prev_snippet_tabstop(cx); +// assert( +// editor, +// cx, +// indoc! {" +// a.f(one, two, three)ˇ b +// a.f(one, two, three)ˇ b +// a.f(one, two, three)ˇ b +// "}, +// ); +// }); // } -// #[gpui::test] -// async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 4); + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )])) + }) + .next() + .await; + cx.executor().start_waiting(); + let x = save.await; + + assert_eq!( + editor.update(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + // Ensure we can still save even if formatting hangs. + fake_server.handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); + cx.executor().advance_clock(super::FORMAT_TIMEOUT); + cx.executor().start_waiting(); + save.await; + assert_eq!( + editor.update(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + // Set rust language override and assert overridden tabsize is sent to language server + update_test_language_settings(cx, |settings| { + settings.languages.insert( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); + }); -// let language = Arc::new(Language::new( -// LanguageConfig { -// line_comment: Some("// ".into()), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 8); + Ok(Some(vec![])) + }) + .next() + .await; + cx.executor().start_waiting(); + save.await; +} -// let registry = Arc::new(LanguageRegistry::test()); -// registry.add(language.clone()); +#[gpui::test] +async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_range_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 4); + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )])) + }) + .next() + .await; + cx.executor().start_waiting(); + save.await; + assert_eq!( + editor.update(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + // Ensure we can still save even if formatting hangs. + fake_server.handle_request::( + move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }, + ); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); + cx.executor().advance_clock(super::FORMAT_TIMEOUT); + cx.executor().start_waiting(); + save.await; + assert_eq!( + editor.update(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + // Set rust language override and assert overridden tabsize is sent to language server + update_test_language_settings(cx, |settings| { + settings.languages.insert( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); + }); -// let mut cx = EditorTestContext::new(cx).await; -// cx.update_buffer(|buffer, cx| { -// buffer.set_language_registry(registry); -// buffer.set_language(Some(language), cx); -// }); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 8); + Ok(Some(vec![])) + }) + .next() + .await; + cx.executor().start_waiting(); + save.await; +} -// let toggle_comments = &ToggleComments { -// advance_downwards: true, -// }; +#[gpui::test] +async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) + }); -// // Single cursor on one line -> advance -// // Cursor moves horizontally 3 characters as well on non-blank line -// cx.set_state(indoc!( -// "fn a() { -// ˇdog(); -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // dog(); -// catˇ(); -// }" -// )); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + // Enable Prettier formatting for the same buffer, and ensure + // LSP is called instead of Prettier. + prettier_parser_name: Some("test_parser".to_string()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + + let format = editor + .update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }) + .unwrap(); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 4); + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )])) + }) + .next() + .await; + cx.executor().start_waiting(); + format.await; + assert_eq!( + editor.update(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + // Ensure we don't lock if formatting hangs. + fake_server.handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }); + let format = editor + .update(cx, |editor, cx| { + editor.perform_format(project, FormatTrigger::Manual, cx) + }) + .unwrap(); + cx.executor().advance_clock(super::FORMAT_TIMEOUT); + cx.executor().start_waiting(); + format.await; + assert_eq!( + editor.update(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); +} -// // Single selection on one line -> don't advance -// cx.set_state(indoc!( -// "fn a() { -// «dog()ˇ»; -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // «dog()ˇ»; -// cat(); -// }" -// )); +#[gpui::test] +async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + one.twoˇ + "}); + + // The format request takes a long time. When it completes, it inserts + // a newline and an indent before the `.` + cx.lsp + .handle_request::(move |_, cx| { + let executor = cx.background_executor().clone(); + async move { + executor.timer(Duration::from_millis(100)).await; + Ok(Some(vec![lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)), + new_text: "\n ".into(), + }])) + } + }); -// // Multiple cursors on one line -> advance -// cx.set_state(indoc!( -// "fn a() { -// ˇdˇog(); -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // dog(); -// catˇ(ˇ); -// }" -// )); + // Submit a format request. + let format_1 = cx + .update_editor(|editor, cx| editor.format(&Format, cx)) + .unwrap(); + cx.executor().run_until_parked(); + + // Submit a second format request. + let format_2 = cx + .update_editor(|editor, cx| editor.format(&Format, cx)) + .unwrap(); + cx.executor().run_until_parked(); + + // Wait for both format requests to complete + cx.executor().advance_clock(Duration::from_millis(200)); + cx.executor().start_waiting(); + format_1.await.unwrap(); + cx.executor().start_waiting(); + format_2.await.unwrap(); + + // The formatting edits only happens once. + cx.assert_editor_state(indoc! {" + one + .twoˇ + "}); +} -// // Multiple cursors on one line, with selection -> don't advance -// cx.set_state(indoc!( -// "fn a() { -// ˇdˇog«()ˇ»; -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // ˇdˇog«()ˇ»; -// cat(); -// }" -// )); +#[gpui::test] +async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Auto) + }); -// // Single cursor on one line -> advance -// // Cursor moves to column 0 on blank line -// cx.set_state(indoc!( -// "fn a() { -// ˇdog(); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Set up a buffer white some trailing whitespace and no trailing newline. + cx.set_state( + &[ + "one ", // + "twoˇ", // + "three ", // + "four", // + ] + .join("\n"), + ); + + // Submit a format request. + let format = cx + .update_editor(|editor, cx| editor.format(&Format, cx)) + .unwrap(); + + // Record which buffer changes have been sent to the language server + let buffer_changes = Arc::new(Mutex::new(Vec::new())); + cx.lsp + .handle_notification::({ + let buffer_changes = buffer_changes.clone(); + move |params, _| { + buffer_changes.lock().extend( + params + .content_changes + .into_iter() + .map(|e| (e.range.unwrap(), e.text)), + ); + } + }); -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // dog(); -// ˇ -// cat(); -// }" -// )); + // Handle formatting requests to the language server. + cx.lsp.handle_request::({ + let buffer_changes = buffer_changes.clone(); + move |_, _| { + // When formatting is requested, trailing whitespace has already been stripped, + // and the trailing newline has already been added. + assert_eq!( + &buffer_changes.lock()[1..], + &[ + ( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)), + "".into() + ), + ( + lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), + "".into() + ), + ( + lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)), + "\n".into() + ), + ] + ); + + // Insert blank lines between each line of the buffer. + async move { + Ok(Some(vec![ + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), + new_text: "\n".into(), + }, + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)), + new_text: "\n".into(), + }, + ])) + } + } + }); -// // Single cursor on one line -> advance -// // Cursor starts and ends at column 0 -// cx.set_state(indoc!( -// "fn a() { -// ˇ dog(); -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // dog(); -// ˇ cat(); -// }" -// )); -// } + // After formatting the buffer, the trailing whitespace is stripped, + // a newline is appended, and the edits provided by the language server + // have been applied. + format.await.unwrap(); + cx.assert_editor_state( + &[ + "one", // + "", // + "twoˇ", // + "", // + "three", // + "four", // + "", // + ] + .join("\n"), + ); + + // Undoing the formatting undoes the trailing whitespace removal, the + // trailing newline, and the LSP edits. + cx.update_buffer(|buffer, cx| buffer.undo(cx)); + cx.assert_editor_state( + &[ + "one ", // + "twoˇ", // + "three ", // + "four", // + ] + .join("\n"), + ); +} +//todo!(completion) // #[gpui::test] -// async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { +// async fn test_completion(cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; - -// let html_language = Arc::new( -// Language::new( -// LanguageConfig { -// name: "HTML".into(), -// block_comment: Some(("".into())), +// let mut cx = EditorLspTestContext::new_rust( +// lsp::ServerCapabilities { +// completion_provider: Some(lsp::CompletionOptions { +// trigger_characters: Some(vec![".".to_string(), ":".to_string()]), +// resolve_provider: Some(true), // ..Default::default() -// }, -// Some(tree_sitter_html::language()), -// ) -// .with_injection_query( -// r#" -// (script_element -// (raw_text) @content -// (#set! "language" "javascript")) -// "#, -// ) -// .unwrap(), -// ); - -// let javascript_language = Arc::new(Language::new( -// LanguageConfig { -// name: "JavaScript".into(), -// line_comment: Some("// ".into()), +// }), // ..Default::default() // }, -// Some(tree_sitter_typescript::language_tsx()), -// )); - -// let registry = Arc::new(LanguageRegistry::test()); -// registry.add(html_language.clone()); -// registry.add(javascript_language.clone()); +// cx, +// ) +// .await; -// cx.update_buffer(|buffer, cx| { -// buffer.set_language_registry(registry); -// buffer.set_language(Some(html_language), cx); +// cx.set_state(indoc! {" +// oneˇ +// two +// three +// "}); +// cx.simulate_keystroke("."); +// handle_completion_request( +// &mut cx, +// indoc! {" +// one.|<> +// two +// three +// "}, +// vec!["first_completion", "second_completion"], +// ) +// .await; +// cx.condition(|editor, _| editor.context_menu_visible()) +// .await; +// let apply_additional_edits = cx.update_editor(|editor, cx| { +// editor.context_menu_next(&Default::default(), cx); +// editor +// .confirm_completion(&ConfirmCompletion::default(), cx) +// .unwrap() // }); +// cx.assert_editor_state(indoc! {" +// one.second_completionˇ +// two +// three +// "}); -// // Toggle comments for empty selections -// cx.set_state( -// &r#" -//

A

ˇ -//

B

ˇ -//

C

ˇ -// "# -// .unindent(), -// ); -// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); -// cx.assert_editor_state( -// &r#" -// -// -// -// "# -// .unindent(), -// ); -// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); -// cx.assert_editor_state( -// &r#" -//

A

ˇ -//

B

ˇ -//

C

ˇ -// "# -// .unindent(), -// ); - -// // Toggle comments for mixture of empty and non-empty selections, where -// // multiple selections occupy a given line. -// cx.set_state( -// &r#" -//

-//

ˇ»B

ˇ -//

-//

ˇ»D

ˇ -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); -// cx.assert_editor_state( -// &r#" -// -// -// "# -// .unindent(), -// ); -// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); -// cx.assert_editor_state( -// &r#" -//

-//

ˇ»B

ˇ -//

-//

ˇ»D

ˇ -// "# -// .unindent(), -// ); - -// // Toggle comments when different languages are active for different -// // selections. -// cx.set_state( -// &r#" -// ˇ -// "# -// .unindent(), -// ); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); -// cx.assert_editor_state( -// &r#" -// -// // ˇvar x = new Y(); -// -// "# -// .unindent(), -// ); -// } - -// #[gpui::test] -// fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +// handle_resolve_completion_request( +// &mut cx, +// Some(vec![ +// ( +// //This overlaps with the primary completion edit which is +// //misbehavior from the LSP spec, test that we filter it out +// indoc! {" +// one.second_ˇcompletion +// two +// threeˇ +// "}, +// "overlapping additional edit", +// ), +// ( +// indoc! {" +// one.second_completion +// two +// threeˇ +// "}, +// "\nadditional edit", +// ), +// ]), +// ) +// .await; +// apply_additional_edits.await.unwrap(); +// cx.assert_editor_state(indoc! {" +// one.second_completionˇ +// two +// three +// additional edit +// "}); -// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// buffer.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(0, 4), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(1, 0)..Point::new(1, 4), -// primary: None, -// }, -// ], -// cx, -// ); -// assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb"); -// multibuffer -// }); +// cx.set_state(indoc! {" +// one.second_completion +// twoˇ +// threeˇ +// additional edit +// "}); +// cx.simulate_keystroke(" "); +// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); +// cx.simulate_keystroke("s"); +// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); -// view.update(cx, |view, cx| { -// assert_eq!(view.text(cx), "aaaa\nbbbb"); -// view.change_selections(None, cx, |s| { -// s.select_ranges([ -// Point::new(0, 0)..Point::new(0, 0), -// Point::new(1, 0)..Point::new(1, 0), -// ]) -// }); +// cx.assert_editor_state(indoc! {" +// one.second_completion +// two sˇ +// three sˇ +// additional edit +// "}); +// handle_completion_request( +// &mut cx, +// indoc! {" +// one.second_completion +// two s +// three +// additional edit +// "}, +// vec!["fourth_completion", "fifth_completion", "sixth_completion"], +// ) +// .await; +// cx.condition(|editor, _| editor.context_menu_visible()) +// .await; -// view.handle_input("X", cx); -// assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); -// assert_eq!( -// view.selections.ranges(cx), -// [ -// Point::new(0, 1)..Point::new(0, 1), -// Point::new(1, 1)..Point::new(1, 1), -// ] -// ); +// cx.simulate_keystroke("i"); -// // Ensure the cursor's head is respected when deleting across an excerpt boundary. -// view.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) -// }); -// view.backspace(&Default::default(), cx); -// assert_eq!(view.text(cx), "Xa\nbbb"); -// assert_eq!( -// view.selections.ranges(cx), -// [Point::new(1, 0)..Point::new(1, 0)] -// ); +// handle_completion_request( +// &mut cx, +// indoc! {" +// one.second_completion +// two si +// three +// additional edit +// "}, +// vec!["fourth_completion", "fifth_completion", "sixth_completion"], +// ) +// .await; +// cx.condition(|editor, _| editor.context_menu_visible()) +// .await; -// view.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) -// }); -// view.backspace(&Default::default(), cx); -// assert_eq!(view.text(cx), "X\nbb"); -// assert_eq!( -// view.selections.ranges(cx), -// [Point::new(0, 1)..Point::new(0, 1)] -// ); +// let apply_additional_edits = cx.update_editor(|editor, cx| { +// editor +// .confirm_completion(&ConfirmCompletion::default(), cx) +// .unwrap() // }); -// } +// cx.assert_editor_state(indoc! {" +// one.second_completion +// two sixth_completionˇ +// three sixth_completionˇ +// additional edit +// "}); -// #[gpui::test] -// fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +// handle_resolve_completion_request(&mut cx, None).await; +// apply_additional_edits.await.unwrap(); -// let markers = vec![('[', ']').into(), ('(', ')').into()]; -// let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( -// indoc! {" -// [aaaa -// (bbbb] -// cccc)", -// }, -// markers.clone(), -// ); -// let excerpt_ranges = markers.into_iter().map(|marker| { -// let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); -// ExcerptRange { -// context, -// primary: None, -// } +// cx.update(|cx| { +// cx.update_global::(|settings, cx| { +// settings.update_user_settings::(cx, |settings| { +// settings.show_completions_on_input = Some(false); +// }); +// }) // }); -// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text)); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts(buffer, excerpt_ranges, cx); -// multibuffer +// cx.set_state("editorˇ"); +// cx.simulate_keystroke("."); +// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); +// cx.simulate_keystroke("c"); +// cx.simulate_keystroke("l"); +// cx.simulate_keystroke("o"); +// cx.assert_editor_state("editor.cloˇ"); +// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); +// cx.update_editor(|editor, cx| { +// editor.show_completions(&ShowCompletions, cx); // }); - -// let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); -// view.update(cx, |view, cx| { -// let (expected_text, selection_ranges) = marked_text_ranges( -// indoc! {" -// aaaa -// bˇbbb -// bˇbbˇb -// cccc" -// }, -// true, -// ); -// assert_eq!(view.text(cx), expected_text); -// view.change_selections(None, cx, |s| s.select_ranges(selection_ranges)); - -// view.handle_input("X", cx); - -// let (expected_text, expected_selections) = marked_text_ranges( -// indoc! {" -// aaaa -// bXˇbbXb -// bXˇbbXˇb -// cccc" -// }, -// false, -// ); -// assert_eq!(view.text(cx), expected_text); -// assert_eq!(view.selections.ranges(cx), expected_selections); - -// view.newline(&Newline, cx); -// let (expected_text, expected_selections) = marked_text_ranges( -// indoc! {" -// aaaa -// bX -// ˇbbX -// b -// bX -// ˇbbX -// ˇb -// cccc" -// }, -// false, -// ); -// assert_eq!(view.text(cx), expected_text); -// assert_eq!(view.selections.ranges(cx), expected_selections); +// handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; +// cx.condition(|editor, _| editor.context_menu_visible()) +// .await; +// let apply_additional_edits = cx.update_editor(|editor, cx| { +// editor +// .confirm_completion(&ConfirmCompletion::default(), cx) +// .unwrap() // }); +// cx.assert_editor_state("editor.closeˇ"); +// handle_resolve_completion_request(&mut cx, None).await; +// apply_additional_edits.await.unwrap(); // } -// #[gpui::test] -// fn test_refresh_selections(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // If multiple selections intersect a line, the line is only toggled once. + cx.set_state(indoc! {" + fn a() { + «//b(); + ˇ»// «c(); + //ˇ» d(); + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + «b(); + c(); + ˇ» d(); + } + "}); + + // The comment prefix is inserted at the same column for every line in a + // selection. + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // «b(); + // c(); + ˇ»// d(); + } + "}); + + // If a selection ends at the beginning of a line, that line is not toggled. + cx.set_selections_state(indoc! {" + fn a() { + // b(); + «// c(); + ˇ» // d(); + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // b(); + «c(); + ˇ» // d(); + } + "}); + + // If a selection span a single line and is empty, the line is toggled. + cx.set_state(indoc! {" + fn a() { + a(); + b(); + ˇ + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + a(); + b(); + //•ˇ + } + "}); + + // If a selection span multiple lines, empty lines are not toggled. + cx.set_state(indoc! {" + fn a() { + «a(); + + c();ˇ» + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // «a(); + + // c();ˇ» + } + "}); +} -// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); -// let mut excerpt1_id = None; -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// excerpt1_id = multibuffer -// .push_excerpts( -// buffer.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 4), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(1, 0)..Point::new(2, 4), -// primary: None, -// }, -// ], -// cx, -// ) -// .into_iter() -// .next(); -// assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); -// multibuffer -// }); +#[gpui::test] +async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(language.clone()); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(language), cx); + }); -// let editor = cx -// .add_window(|cx| { -// let mut editor = build_editor(multibuffer.clone(), cx); -// let snapshot = editor.snapshot(cx); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) -// }); -// editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); -// assert_eq!( -// editor.selections.ranges(cx), -// [ -// Point::new(1, 3)..Point::new(1, 3), -// Point::new(2, 1)..Point::new(2, 1), -// ] -// ); -// editor -// }) -// .root(cx); + let toggle_comments = &ToggleComments { + advance_downwards: true, + }; + + // Single cursor on one line -> advance + // Cursor moves horizontally 3 characters as well on non-blank line + cx.set_state(indoc!( + "fn a() { + ˇdog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + catˇ(); + }" + )); + + // Single selection on one line -> don't advance + cx.set_state(indoc!( + "fn a() { + «dog()ˇ»; + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // «dog()ˇ»; + cat(); + }" + )); + + // Multiple cursors on one line -> advance + cx.set_state(indoc!( + "fn a() { + ˇdˇog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + catˇ(ˇ); + }" + )); + + // Multiple cursors on one line, with selection -> don't advance + cx.set_state(indoc!( + "fn a() { + ˇdˇog«()ˇ»; + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // ˇdˇog«()ˇ»; + cat(); + }" + )); + + // Single cursor on one line -> advance + // Cursor moves to column 0 on blank line + cx.set_state(indoc!( + "fn a() { + ˇdog(); + + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + ˇ + cat(); + }" + )); + + // Single cursor on one line -> advance + // Cursor starts and ends at column 0 + cx.set_state(indoc!( + "fn a() { + ˇ dog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + ˇ cat(); + }" + )); +} -// // Refreshing selections is a no-op when excerpts haven't changed. -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.refresh()); -// assert_eq!( -// editor.selections.ranges(cx), -// [ -// Point::new(1, 3)..Point::new(1, 3), -// Point::new(2, 1)..Point::new(2, 1), -// ] -// ); -// }); +#[gpui::test] +async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let html_language = Arc::new( + Language::new( + LanguageConfig { + name: "HTML".into(), + block_comment: Some(("".into())), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_injection_query( + r#" + (script_element + (raw_text) @content + (#set! "language" "javascript")) + "#, + ) + .unwrap(), + ); + + let javascript_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(html_language.clone()); + registry.add(javascript_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(html_language), cx); + }); -// multibuffer.update(cx, |multibuffer, cx| { -// multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); -// }); -// editor.update(cx, |editor, cx| { -// // Removing an excerpt causes the first selection to become degenerate. -// assert_eq!( -// editor.selections.ranges(cx), -// [ -// Point::new(0, 0)..Point::new(0, 0), -// Point::new(0, 1)..Point::new(0, 1) -// ] -// ); + // Toggle comments for empty selections + cx.set_state( + &r#" +

A

ˇ +

B

ˇ +

C

ˇ + "# + .unindent(), + ); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.assert_editor_state( + &r#" +

A

ˇ +

B

ˇ +

C

ˇ + "# + .unindent(), + ); + + // Toggle comments for mixture of empty and non-empty selections, where + // multiple selections occupy a given line. + cx.set_state( + &r#" +

+

ˇ»B

ˇ +

+

ˇ»D

ˇ + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.assert_editor_state( + &r#" + + + "# + .unindent(), + ); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.assert_editor_state( + &r#" +

+

ˇ»B

ˇ +

+

ˇ»D

ˇ + "# + .unindent(), + ); + + // Toggle comments when different languages are active for different + // selections. + cx.set_state( + &r#" + ˇ + "# + .unindent(), + ); + cx.executor().run_until_parked(); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.assert_editor_state( + &r#" + + // ˇvar x = new Y(); + + "# + .unindent(), + ); +} -// // Refreshing selections will relocate the first selection to the original buffer -// // location. -// editor.change_selections(None, cx, |s| s.refresh()); -// assert_eq!( -// editor.selections.ranges(cx), -// [ -// Point::new(0, 1)..Point::new(0, 1), -// Point::new(0, 3)..Point::new(0, 3) -// ] -// ); -// assert!(editor.selections.pending_anchor().is_some()); -// }); -// } +#[gpui::test] +fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); + 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(0, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(1, 0)..Point::new(1, 4), + primary: None, + }, + ], + cx, + ); + assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb"); + multibuffer + }); -// #[gpui::test] -// fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); + let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); + let cx = &mut cx; + view.update(cx, |view, cx| { + assert_eq!(view.text(cx), "aaaa\nbbbb"); + view.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 0)..Point::new(0, 0), + Point::new(1, 0)..Point::new(1, 0), + ]) + }); -// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); -// let mut excerpt1_id = None; -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// excerpt1_id = multibuffer -// .push_excerpts( -// buffer.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 4), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(1, 0)..Point::new(2, 4), -// primary: None, -// }, -// ], -// cx, -// ) -// .into_iter() -// .next(); -// assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); -// multibuffer -// }); + view.handle_input("X", cx); + assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); + assert_eq!( + view.selections.ranges(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + ] + ); + + // Ensure the cursor's head is respected when deleting across an excerpt boundary. + view.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) + }); + view.backspace(&Default::default(), cx); + assert_eq!(view.text(cx), "Xa\nbbb"); + assert_eq!( + view.selections.ranges(cx), + [Point::new(1, 0)..Point::new(1, 0)] + ); + + view.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) + }); + view.backspace(&Default::default(), cx); + assert_eq!(view.text(cx), "X\nbb"); + assert_eq!( + view.selections.ranges(cx), + [Point::new(0, 1)..Point::new(0, 1)] + ); + }); +} -// let editor = cx -// .add_window(|cx| { -// let mut editor = build_editor(multibuffer.clone(), cx); -// let snapshot = editor.snapshot(cx); -// editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); -// assert_eq!( -// editor.selections.ranges(cx), -// [Point::new(1, 3)..Point::new(1, 3)] -// ); -// editor -// }) -// .root(cx); +#[gpui::test] +fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let markers = vec![('[', ']').into(), ('(', ')').into()]; + let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( + indoc! {" + [aaaa + (bbbb] + cccc)", + }, + markers.clone(), + ); + let excerpt_ranges = markers.into_iter().map(|marker| { + let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); + ExcerptRange { + context, + primary: None, + } + }); + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text)); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts(buffer, excerpt_ranges, cx); + multibuffer + }); -// multibuffer.update(cx, |multibuffer, cx| { -// multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); -// }); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// editor.selections.ranges(cx), -// [Point::new(0, 0)..Point::new(0, 0)] -// ); + let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); + let cx = &mut cx; + view.update(cx, |view, cx| { + let (expected_text, selection_ranges) = marked_text_ranges( + indoc! {" + aaaa + bˇbbb + bˇbbˇb + cccc" + }, + true, + ); + assert_eq!(view.text(cx), expected_text); + view.change_selections(None, cx, |s| s.select_ranges(selection_ranges)); + + view.handle_input("X", cx); + + let (expected_text, expected_selections) = marked_text_ranges( + indoc! {" + aaaa + bXˇbbXb + bXˇbbXˇb + cccc" + }, + false, + ); + assert_eq!(view.text(cx), expected_text); + assert_eq!(view.selections.ranges(cx), expected_selections); + + view.newline(&Newline, cx); + let (expected_text, expected_selections) = marked_text_ranges( + indoc! {" + aaaa + bX + ˇbbX + b + bX + ˇbbX + ˇb + cccc" + }, + false, + ); + assert_eq!(view.text(cx), expected_text); + assert_eq!(view.selections.ranges(cx), expected_selections); + }); +} -// // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. -// editor.change_selections(None, cx, |s| s.refresh()); -// assert_eq!( -// editor.selections.ranges(cx), -// [Point::new(0, 3)..Point::new(0, 3)] -// ); -// assert!(editor.selections.pending_anchor().is_some()); -// }); -// } +#[gpui::test] +fn test_refresh_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); + let mut excerpt1_id = None; + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + excerpt1_id = multibuffer + .push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 4), + primary: None, + }, + ], + cx, + ) + .into_iter() + .next(); + assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); + multibuffer + }); -// #[gpui::test] -// async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + let editor = cx.add_window(|cx| { + let mut editor = build_editor(multibuffer.clone(), cx); + let snapshot = editor.snapshot(cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) + }); + editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ] + ); + editor + }); -// let language = Arc::new( -// Language::new( -// LanguageConfig { -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }, -// BracketPair { -// start: "/* ".to_string(), -// end: " */".to_string(), -// close: true, -// newline: true, -// }, -// ], -// ..Default::default() -// }, -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query("") -// .unwrap(), -// ); + // Refreshing selections is a no-op when excerpts haven't changed. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.refresh()); + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ] + ); + }); -// let text = concat!( -// "{ }\n", // -// " x\n", // -// " /* */\n", // -// "x\n", // -// "{{} }\n", // -// ); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); + }); + editor.update(cx, |editor, cx| { + // Removing an excerpt causes the first selection to become degenerate. + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(0, 0)..Point::new(0, 0), + Point::new(0, 1)..Point::new(0, 1) + ] + ); + + // Refreshing selections will relocate the first selection to the original buffer + // location. + editor.change_selections(None, cx, |s| s.refresh()); + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(0, 3)..Point::new(0, 3) + ] + ); + assert!(editor.selections.pending_anchor().is_some()); + }); +} -// let buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) -// .await; +#[gpui::test] +fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); + let mut excerpt1_id = None; + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + excerpt1_id = multibuffer + .push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 4), + primary: None, + }, + ], + cx, + ) + .into_iter() + .next(); + assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); + multibuffer + }); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), -// DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), -// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), -// ]) -// }); -// view.newline(&Newline, cx); + let editor = cx.add_window(|cx| { + let mut editor = build_editor(multibuffer.clone(), cx); + let snapshot = editor.snapshot(cx); + editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); + assert_eq!( + editor.selections.ranges(cx), + [Point::new(1, 3)..Point::new(1, 3)] + ); + editor + }); -// assert_eq!( -// view.buffer().read(cx).read(cx).text(), -// concat!( -// "{ \n", // Suppress rustfmt -// "\n", // -// "}\n", // -// " x\n", // -// " /* \n", // -// " \n", // -// " */\n", // -// "x\n", // -// "{{} \n", // -// "}\n", // -// ) -// ); -// }); -// } + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); + }); + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.ranges(cx), + [Point::new(0, 0)..Point::new(0, 0)] + ); + + // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. + editor.change_selections(None, cx, |s| s.refresh()); + assert_eq!( + editor.selections.ranges(cx), + [Point::new(0, 3)..Point::new(0, 3)] + ); + assert!(editor.selections.pending_anchor().is_some()); + }); +} + +#[gpui::test] +async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/* ".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query("") + .unwrap(), + ); + + let text = concat!( + "{ }\n", // + " x\n", // + " /* */\n", // + "x\n", // + "{{} }\n", // + ); + + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; + view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ]) + }); + view.newline(&Newline, cx); + + assert_eq!( + view.buffer().read(cx).read(cx).text(), + concat!( + "{ \n", // Suppress rustfmt + "\n", // + "}\n", // + " x\n", // + " /* \n", // + " \n", // + " */\n", // + "x\n", // + "{{} \n", // + "}\n", // + ) + ); + }); +} +//todo!(finish editor tests) // #[gpui::test] // fn test_highlighted_ranges(cx: &mut TestAppContext) { // init_test(cx, |_| {}); -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); +// let editor = cx.add_window(|cx| { +// let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); +// build_editor(buffer.clone(), cx) +// }); // editor.update(cx, |editor, cx| { // struct Type1; @@ -6298,10 +6379,10 @@ use settings::SettingsStore; // let mut highlighted_ranges = editor.background_highlights_in_range( // anchor_range(Point::new(3, 4)..Point::new(7, 4)), // &snapshot, -// theme::current(cx).as_ref(), +// cx.theme().colors(), // ); // // Enforce a consistent ordering based on color without relying on the ordering of the -// // highlight's `TypeId` which is non-deterministic. +// // highlight's `TypeId` which is non-executor. // highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); // assert_eq!( // highlighted_ranges, @@ -6328,7 +6409,7 @@ use settings::SettingsStore; // editor.background_highlights_in_range( // anchor_range(Point::new(5, 6)..Point::new(6, 4)), // &snapshot, -// theme::current(cx).as_ref(), +// cx.theme().colors(), // ), // &[( // DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), @@ -6338,33 +6419,33 @@ use settings::SettingsStore; // }); // } +// todo!(following) // #[gpui::test] // async fn test_following(cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); -// let fs = FakeFs::new(cx.background()); +// let fs = FakeFs::new(cx.executor()); // let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; // let buffer = project.update(cx, |project, cx| { // let buffer = project // .create_buffer(&sample_text(16, 8, 'a'), None, cx) // .unwrap(); -// cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) +// cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)) +// }); +// let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx)); +// let follower = cx.update(|cx| { +// cx.open_window( +// WindowOptions { +// bounds: WindowBounds::Fixed(Bounds::from_corners( +// gpui::Point::new((0. as f64).into(), (0. as f64).into()), +// gpui::Point::new((10. as f64).into(), (80. as f64).into()), +// )), +// ..Default::default() +// }, +// |cx| cx.build_view(|cx| build_editor(buffer.clone(), cx)), +// ) // }); -// let leader = cx -// .add_window(|cx| build_editor(buffer.clone(), cx)) -// .root(cx); -// let follower = cx -// .update(|cx| { -// cx.add_window( -// WindowOptions { -// bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), -// ..Default::default() -// }, -// |cx| build_editor(buffer.clone(), cx), -// ) -// }) -// .root(cx); // let is_still_following = Rc::new(RefCell::new(true)); // let follower_edit_event_count = Rc::new(RefCell::new(0)); @@ -6374,21 +6455,28 @@ use settings::SettingsStore; // let is_still_following = is_still_following.clone(); // let follower_edit_event_count = follower_edit_event_count.clone(); // |_, cx| { -// cx.subscribe(&leader, move |_, leader, event, cx| { -// leader -// .read(cx) -// .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); -// }) +// cx.subscribe( +// &leader.root_view(cx).unwrap(), +// move |_, leader, event, cx| { +// leader +// .read(cx) +// .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); +// }, +// ) // .detach(); -// cx.subscribe(&follower, move |_, _, event, cx| { -// if Editor::should_unfollow_on_event(event, cx) { -// *is_still_following.borrow_mut() = false; -// } -// if let Event::BufferEdited = event { -// *follower_edit_event_count.borrow_mut() += 1; -// } -// }) +// cx.subscribe( +// &follower.root_view(cx).unwrap(), +// move |_, _, event: &Event, cx| { +// if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { +// *is_still_following.borrow_mut() = false; +// } + +// if let Event::BufferEdited = event { +// *follower_edit_event_count.borrow_mut() += 1; +// } +// }, +// ) // .detach(); // } // }); @@ -6401,9 +6489,10 @@ use settings::SettingsStore; // .update(cx, |follower, cx| { // follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) // }) +// .unwrap() // .await // .unwrap(); -// follower.read_with(cx, |follower, cx| { +// follower.update(cx, |follower, cx| { // assert_eq!(follower.selections.ranges(cx), vec![1..1]); // }); // assert_eq!(*is_still_following.borrow(), true); @@ -6411,17 +6500,20 @@ use settings::SettingsStore; // // Update the scroll position only // leader.update(cx, |leader, cx| { -// leader.set_scroll_position(vec2f(1.5, 3.5), cx); +// leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); // }); // follower // .update(cx, |follower, cx| { // follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) // }) +// .unwrap() // .await // .unwrap(); // assert_eq!( -// follower.update(cx, |follower, cx| follower.scroll_position(cx)), -// vec2f(1.5, 3.5) +// follower +// .update(cx, |follower, cx| follower.scroll_position(cx)) +// .unwrap(), +// gpui::Point::new(1.5, 3.5) // ); // assert_eq!(*is_still_following.borrow(), true); // assert_eq!(*follower_edit_event_count.borrow(), 0); @@ -6431,16 +6523,17 @@ use settings::SettingsStore; // leader.update(cx, |leader, cx| { // leader.change_selections(None, cx, |s| s.select_ranges([0..0])); // leader.request_autoscroll(Autoscroll::newest(), cx); -// leader.set_scroll_position(vec2f(1.5, 3.5), cx); +// leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); // }); // follower // .update(cx, |follower, cx| { // follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) // }) +// .unwrap() // .await // .unwrap(); // follower.update(cx, |follower, cx| { -// assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0)); +// assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0)); // assert_eq!(follower.selections.ranges(cx), vec![0..0]); // }); // assert_eq!(*is_still_following.borrow(), true); @@ -6454,9 +6547,10 @@ use settings::SettingsStore; // .update(cx, |follower, cx| { // follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) // }) +// .unwrap() // .await // .unwrap(); -// follower.read_with(cx, |follower, cx| { +// follower.update(cx, |follower, cx| { // assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); // }); // assert_eq!(*is_still_following.borrow(), true); @@ -6469,9 +6563,10 @@ use settings::SettingsStore; // .update(cx, |follower, cx| { // follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) // }) +// .unwrap() // .await // .unwrap(); -// follower.read_with(cx, |follower, cx| { +// follower.update(cx, |follower, cx| { // assert_eq!(follower.selections.ranges(cx), vec![0..2]); // }); @@ -6481,7 +6576,7 @@ use settings::SettingsStore; // follower.set_scroll_anchor( // ScrollAnchor { // anchor: top_anchor, -// offset: vec2f(0.0, 0.5), +// offset: gpui::Point::new(0.0, 0.5), // }, // cx, // ); @@ -6493,16 +6588,18 @@ use settings::SettingsStore; // async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); -// let fs = FakeFs::new(cx.background()); +// let fs = FakeFs::new(cx.executor()); // let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); +// let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let pane = workspace +// .update(cx, |workspace, _| workspace.active_pane().clone()) +// .unwrap(); + +// let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); // let leader = pane.update(cx, |_, cx| { -// let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); -// cx.add_view(|cx| build_editor(multibuffer.clone(), cx)) +// let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); +// cx.build_view(|cx| build_editor(multibuffer.clone(), cx)) // }); // // Start following the editor when it has no excerpts. @@ -6511,7 +6608,7 @@ use settings::SettingsStore; // .update(|cx| { // Editor::from_state_proto( // pane.clone(), -// workspace.clone(), +// workspace.root_view(cx).unwrap(), // ViewId { // creator: Default::default(), // id: 0, @@ -6595,8 +6692,8 @@ use settings::SettingsStore; // .await // .unwrap(); // assert_eq!( -// follower_1.read_with(cx, |editor, cx| editor.text(cx)), -// leader.read_with(cx, |editor, cx| editor.text(cx)) +// follower_1.update(cx, |editor, cx| editor.text(cx)), +// leader.update(cx, |editor, cx| editor.text(cx)) // ); // update_message.borrow_mut().take(); @@ -6619,8 +6716,8 @@ use settings::SettingsStore; // .await // .unwrap(); // assert_eq!( -// follower_2.read_with(cx, |editor, cx| editor.text(cx)), -// leader.read_with(cx, |editor, cx| editor.text(cx)) +// follower_2.update(cx, |editor, cx| editor.text(cx)), +// leader.update(cx, |editor, cx| editor.text(cx)) // ); // // Remove some excerpts. @@ -6647,385 +6744,390 @@ use settings::SettingsStore; // .unwrap(); // update_message.borrow_mut().take(); // assert_eq!( -// follower_1.read_with(cx, |editor, cx| editor.text(cx)), -// leader.read_with(cx, |editor, cx| editor.text(cx)) -// ); -// } - -// #[test] -// fn test_combine_syntax_and_fuzzy_match_highlights() { -// let string = "abcdefghijklmnop"; -// let syntax_ranges = [ -// ( -// 0..3, -// HighlightStyle { -// color: Some(Hsla::red()), -// ..Default::default() -// }, -// ), -// ( -// 4..8, -// HighlightStyle { -// color: Some(Hsla::green()), -// ..Default::default() -// }, -// ), -// ]; -// let match_indices = [4, 6, 7, 8]; -// assert_eq!( -// combine_syntax_and_fuzzy_match_highlights( -// string, -// Default::default(), -// syntax_ranges.into_iter(), -// &match_indices, -// ), -// &[ -// ( -// 0..3, -// HighlightStyle { -// color: Some(Hsla::red()), -// ..Default::default() -// }, -// ), -// ( -// 4..5, -// HighlightStyle { -// color: Some(Hsla::green()), -// weight: Some(fonts::Weight::BOLD), -// ..Default::default() -// }, -// ), -// ( -// 5..6, -// HighlightStyle { -// color: Some(Hsla::green()), -// ..Default::default() -// }, -// ), -// ( -// 6..8, -// HighlightStyle { -// color: Some(Hsla::green()), -// weight: Some(fonts::Weight::BOLD), -// ..Default::default() -// }, -// ), -// ( -// 8..9, -// HighlightStyle { -// weight: Some(fonts::Weight::BOLD), -// ..Default::default() -// }, -// ), -// ] +// follower_1.update(cx, |editor, cx| editor.text(cx)), +// leader.update(cx, |editor, cx| editor.text(cx)) // ); // } -// #[gpui::test] -// async fn go_to_prev_overlapping_diagnostic( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; -// let project = cx.update_editor(|editor, _| editor.project.clone().unwrap()); - -// cx.set_state(indoc! {" -// ˇfn func(abc def: i32) -> u32 { -// } -// "}); - -// cx.update(|cx| { -// project.update(cx, |project, cx| { -// project -// .update_diagnostics( -// LanguageServerId(0), -// lsp::PublishDiagnosticsParams { -// uri: lsp::Url::from_file_path("/root/file").unwrap(), -// version: None, -// diagnostics: vec![ -// lsp::Diagnostic { -// range: lsp::Range::new( -// lsp::Position::new(0, 11), -// lsp::Position::new(0, 12), -// ), -// severity: Some(lsp::DiagnosticSeverity::ERROR), -// ..Default::default() -// }, -// lsp::Diagnostic { -// range: lsp::Range::new( -// lsp::Position::new(0, 12), -// lsp::Position::new(0, 15), -// ), -// severity: Some(lsp::DiagnosticSeverity::ERROR), -// ..Default::default() -// }, -// lsp::Diagnostic { -// range: lsp::Range::new( -// lsp::Position::new(0, 25), -// lsp::Position::new(0, 28), -// ), -// severity: Some(lsp::DiagnosticSeverity::ERROR), -// ..Default::default() -// }, -// ], -// }, -// &[], -// cx, -// ) -// .unwrap() -// }); -// }); +#[test] +fn test_combine_syntax_and_fuzzy_match_highlights() { + let string = "abcdefghijklmnop"; + let syntax_ranges = [ + ( + 0..3, + HighlightStyle { + color: Some(Hsla::red()), + ..Default::default() + }, + ), + ( + 4..8, + HighlightStyle { + color: Some(Hsla::green()), + ..Default::default() + }, + ), + ]; + let match_indices = [4, 6, 7, 8]; + assert_eq!( + combine_syntax_and_fuzzy_match_highlights( + string, + Default::default(), + syntax_ranges.into_iter(), + &match_indices, + ), + &[ + ( + 0..3, + HighlightStyle { + color: Some(Hsla::red()), + ..Default::default() + }, + ), + ( + 4..5, + HighlightStyle { + color: Some(Hsla::green()), + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + }, + ), + ( + 5..6, + HighlightStyle { + color: Some(Hsla::green()), + ..Default::default() + }, + ), + ( + 6..8, + HighlightStyle { + color: Some(Hsla::green()), + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + }, + ), + ( + 8..9, + HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + }, + ), + ] + ); +} -// deterministic.run_until_parked(); +#[gpui::test] +async fn go_to_prev_overlapping_diagnostic( + executor: BackgroundExecutor, + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |_| {}); -// cx.update_editor(|editor, cx| { -// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); -// }); + let mut cx = EditorTestContext::new(cx).await; + let project = cx.update_editor(|editor, _| editor.project.clone().unwrap()); -// cx.assert_editor_state(indoc! {" -// fn func(abc def: i32) -> ˇu32 { -// } -// "}); + cx.set_state(indoc! {" + ˇfn func(abc def: i32) -> u32 { + } + "}); -// cx.update_editor(|editor, cx| { -// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); -// }); + cx.update(|cx| { + project.update(cx, |project, cx| { + project + .update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/root/file").unwrap(), + version: None, + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 11), + lsp::Position::new(0, 12), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 12), + lsp::Position::new(0, 15), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 25), + lsp::Position::new(0, 28), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + ], + }, + &[], + cx, + ) + .unwrap() + }); + }); -// cx.assert_editor_state(indoc! {" -// fn func(abc ˇdef: i32) -> u32 { -// } -// "}); + executor.run_until_parked(); -// cx.update_editor(|editor, cx| { -// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); -// }); + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); -// cx.assert_editor_state(indoc! {" -// fn func(abcˇ def: i32) -> u32 { -// } -// "}); + cx.assert_editor_state(indoc! {" + fn func(abc def: i32) -> ˇu32 { + } + "}); -// cx.update_editor(|editor, cx| { -// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); -// }); + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); -// cx.assert_editor_state(indoc! {" -// fn func(abc def: i32) -> ˇu32 { -// } -// "}); -// } + cx.assert_editor_state(indoc! {" + fn func(abc ˇdef: i32) -> u32 { + } + "}); -// #[gpui::test] -// async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); -// let mut cx = EditorTestContext::new(cx).await; + cx.assert_editor_state(indoc! {" + fn func(abcˇ def: i32) -> u32 { + } + "}); -// let diff_base = r#" -// use some::mod; + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); -// const A: u32 = 42; + cx.assert_editor_state(indoc! {" + fn func(abc def: i32) -> ˇu32 { + } + "}); +} -// fn main() { -// println!("hello"); +#[gpui::test] +async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// println!("world"); -// } -// "# -// .unindent(); + let mut cx = EditorTestContext::new(cx).await; -// // Edits are modified, removed, modified, added -// cx.set_state( -// &r#" -// use some::modified; + let diff_base = r#" + use some::mod; -// ˇ -// fn main() { -// println!("hello there"); + const A: u32 = 42; -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); + fn main() { + println!("hello"); -// cx.set_diff_base(Some(&diff_base)); -// deterministic.run_until_parked(); + println!("world"); + } + "# + .unindent(); -// cx.update_editor(|editor, cx| { -// //Wrap around the bottom of the buffer -// for _ in 0..3 { -// editor.go_to_hunk(&GoToHunk, cx); -// } -// }); + // Edits are modified, removed, modified, added + cx.set_state( + &r#" + use some::modified; -// cx.assert_editor_state( -// &r#" -// ˇuse some::modified; + ˇ + fn main() { + println!("hello there"); -// fn main() { -// println!("hello there"); + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); + cx.set_diff_base(Some(&diff_base)); + executor.run_until_parked(); -// cx.update_editor(|editor, cx| { -// //Wrap around the top of the buffer -// for _ in 0..2 { -// editor.go_to_prev_hunk(&GoToPrevHunk, cx); -// } -// }); + cx.update_editor(|editor, cx| { + //Wrap around the bottom of the buffer + for _ in 0..3 { + editor.go_to_hunk(&GoToHunk, cx); + } + }); -// cx.assert_editor_state( -// &r#" -// use some::modified; + cx.assert_editor_state( + &r#" + ˇuse some::modified; -// fn main() { -// ˇ println!("hello there"); -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); + fn main() { + println!("hello there"); -// cx.update_editor(|editor, cx| { -// editor.go_to_prev_hunk(&GoToPrevHunk, cx); -// }); + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); -// cx.assert_editor_state( -// &r#" -// use some::modified; + cx.update_editor(|editor, cx| { + //Wrap around the top of the buffer + for _ in 0..2 { + editor.go_to_prev_hunk(&GoToPrevHunk, cx); + } + }); -// ˇ -// fn main() { -// println!("hello there"); + cx.assert_editor_state( + &r#" + use some::modified; -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); -// cx.update_editor(|editor, cx| { -// for _ in 0..3 { -// editor.go_to_prev_hunk(&GoToPrevHunk, cx); -// } -// }); + fn main() { + ˇ println!("hello there"); -// cx.assert_editor_state( -// &r#" -// use some::modified; + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); -// fn main() { -// ˇ println!("hello there"); + cx.update_editor(|editor, cx| { + editor.go_to_prev_hunk(&GoToPrevHunk, cx); + }); -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); + cx.assert_editor_state( + &r#" + use some::modified; + + ˇ + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + for _ in 0..3 { + editor.go_to_prev_hunk(&GoToPrevHunk, cx); + } + }); -// cx.update_editor(|editor, cx| { -// editor.fold(&Fold, cx); + cx.assert_editor_state( + &r#" + use some::modified; -// //Make sure that the fold only gets one hunk -// for _ in 0..4 { -// editor.go_to_hunk(&GoToHunk, cx); -// } -// }); -// cx.assert_editor_state( -// &r#" -// ˇuse some::modified; + fn main() { + ˇ println!("hello there"); -// fn main() { -// println!("hello there"); + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); -// } + cx.update_editor(|editor, cx| { + editor.fold(&Fold, cx); -// #[test] -// fn test_split_words() { -// fn split<'a>(text: &'a str) -> Vec<&'a str> { -// split_words(text).collect() -// } - -// assert_eq!(split("HelloWorld"), &["Hello", "World"]); -// assert_eq!(split("hello_world"), &["hello_", "world"]); -// assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]); -// assert_eq!(split("Hello_World"), &["Hello_", "World"]); -// assert_eq!(split("helloWOrld"), &["hello", "WOrld"]); -// assert_eq!(split("helloworld"), &["helloworld"]); -// } + //Make sure that the fold only gets one hunk + for _ in 0..4 { + editor.go_to_hunk(&GoToHunk, cx); + } + }); -// #[gpui::test] -// async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + cx.assert_editor_state( + &r#" + ˇuse some::modified; -// let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; -// let mut assert = |before, after| { -// let _state_context = cx.set_state(before); -// cx.update_editor(|editor, cx| { -// editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx) -// }); -// cx.assert_editor_state(after); -// }; -// // Outside bracket jumps to outside of matching bracket -// assert("console.logˇ(var);", "console.log(var)ˇ;"); -// assert("console.log(var)ˇ;", "console.logˇ(var);"); + fn main() { + println!("hello there"); -// // Inside bracket jumps to inside of matching bracket -// assert("console.log(ˇvar);", "console.log(varˇ);"); -// assert("console.log(varˇ);", "console.log(ˇvar);"); + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); +} -// // When outside a bracket and inside, favor jumping to the inside bracket -// assert( -// "console.log('foo', [1, 2, 3]ˇ);", -// "console.log(ˇ'foo', [1, 2, 3]);", -// ); -// assert( -// "console.log(ˇ'foo', [1, 2, 3]);", -// "console.log('foo', [1, 2, 3]ˇ);", -// ); +#[test] +fn test_split_words() { + fn split<'a>(text: &'a str) -> Vec<&'a str> { + split_words(text).collect() + } + + assert_eq!(split("HelloWorld"), &["Hello", "World"]); + assert_eq!(split("hello_world"), &["hello_", "world"]); + assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]); + assert_eq!(split("Hello_World"), &["Hello_", "World"]); + assert_eq!(split("helloWOrld"), &["hello", "WOrld"]); + assert_eq!(split("helloworld"), &["helloworld"]); +} -// // Bias forward if two options are equally likely -// assert( -// "let result = curried_fun()ˇ();", -// "let result = curried_fun()()ˇ;", -// ); +#[gpui::test] +async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller -// assert( -// indoc! {" -// function test() { -// console.log('test')ˇ -// }"}, -// indoc! {" -// function test() { -// console.logˇ('test') -// }"}, -// ); -// } + let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; + let mut assert = |before, after| { + let _state_context = cx.set_state(before); + cx.update_editor(|editor, cx| { + editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx) + }); + cx.assert_editor_state(after); + }; + + // Outside bracket jumps to outside of matching bracket + assert("console.logˇ(var);", "console.log(var)ˇ;"); + assert("console.log(var)ˇ;", "console.logˇ(var);"); + + // Inside bracket jumps to inside of matching bracket + assert("console.log(ˇvar);", "console.log(varˇ);"); + assert("console.log(varˇ);", "console.log(ˇvar);"); + + // When outside a bracket and inside, favor jumping to the inside bracket + assert( + "console.log('foo', [1, 2, 3]ˇ);", + "console.log(ˇ'foo', [1, 2, 3]);", + ); + assert( + "console.log(ˇ'foo', [1, 2, 3]);", + "console.log('foo', [1, 2, 3]ˇ);", + ); + + // Bias forward if two options are equally likely + assert( + "let result = curried_fun()ˇ();", + "let result = curried_fun()()ˇ;", + ); + + // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller + assert( + indoc! {" + function test() { + console.log('test')ˇ + }"}, + indoc! {" + function test() { + console.logˇ('test') + }"}, + ); +} +// todo!(completions) // #[gpui::test(iterations = 10)] -// async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppContext) { +// async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); // let (copilot, copilot_lsp) = Copilot::fake(cx); @@ -7067,7 +7169,7 @@ use settings::SettingsStore; // }], // vec![], // ); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); // cx.update_editor(|editor, cx| { // assert!(editor.context_menu_visible()); // assert!(!editor.has_active_copilot_suggestion(cx)); @@ -7109,7 +7211,7 @@ use settings::SettingsStore; // }], // vec![], // ); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); // cx.update_editor(|editor, cx| { // assert!(!editor.context_menu_visible()); // assert!(editor.has_active_copilot_suggestion(cx)); @@ -7142,7 +7244,7 @@ use settings::SettingsStore; // }], // vec![], // ); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); // cx.update_editor(|editor, cx| { // assert!(editor.context_menu_visible()); // assert!(!editor.has_active_copilot_suggestion(cx)); @@ -7157,7 +7259,7 @@ use settings::SettingsStore; // // Ensure existing completion is interpolated when inserting again. // cx.simulate_keystroke("c"); -// deterministic.run_until_parked(); +// executor.run_until_parked(); // cx.update_editor(|editor, cx| { // assert!(!editor.context_menu_visible()); // assert!(editor.has_active_copilot_suggestion(cx)); @@ -7175,7 +7277,7 @@ use settings::SettingsStore; // }], // vec![], // ); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); // cx.update_editor(|editor, cx| { // assert!(!editor.context_menu_visible()); // assert!(editor.has_active_copilot_suggestion(cx)); @@ -7254,106 +7356,104 @@ use settings::SettingsStore; // ); // cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); -// assert_eq!(editor.text(cx), "fn foo() {\n \n}"); - -// // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. -// editor.tab(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.text(cx), "fn foo() {\n \n}"); -// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); - -// // Tabbing again accepts the suggestion. -// editor.tab(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); -// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); -// }); -// } - -// #[gpui::test] -// async fn test_copilot_completion_invalidation( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |_| {}); - -// let (copilot, copilot_lsp) = Copilot::fake(cx); -// cx.update(|cx| cx.set_global(copilot)); -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string(), ":".to_string()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.set_state(indoc! {" -// one -// twˇ -// three -// "}); - -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "two.foo()".into(), -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), -// ..Default::default() -// }], -// vec![], -// ); -// cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); // cx.update_editor(|editor, cx| { // assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); -// assert_eq!(editor.text(cx), "one\ntw\nthree\n"); - -// editor.backspace(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); -// assert_eq!(editor.text(cx), "one\nt\nthree\n"); +// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); +// assert_eq!(editor.text(cx), "fn foo() {\n \n}"); -// editor.backspace(&Default::default(), cx); +// // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. +// editor.tab(&Default::default(), cx); // assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); -// assert_eq!(editor.text(cx), "one\n\nthree\n"); +// assert_eq!(editor.text(cx), "fn foo() {\n \n}"); +// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); -// // Deleting across the original suggestion range invalidates it. -// editor.backspace(&Default::default(), cx); +// // Tabbing again accepts the suggestion. +// editor.tab(&Default::default(), cx); // assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one\nthree\n"); -// assert_eq!(editor.text(cx), "one\nthree\n"); - -// // Undoing the deletion restores the suggestion. -// editor.undo(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); -// assert_eq!(editor.text(cx), "one\n\nthree\n"); +// assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); +// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); // }); // } +#[gpui::test] +async fn test_copilot_completion_invalidation( + executor: BackgroundExecutor, + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |_| {}); + + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + one + twˇ + three + "}); + + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "two.foo()".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), + ..Default::default() + }], + vec![], + ); + cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\ntw\nthree\n"); + + editor.backspace(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\nt\nthree\n"); + + editor.backspace(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\n\nthree\n"); + + // Deleting across the original suggestion range invalidates it. + editor.backspace(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\nthree\n"); + assert_eq!(editor.text(cx), "one\nthree\n"); + + // Undoing the deletion restores the suggestion. + editor.undo(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\n\nthree\n"); + }); +} + +//todo!() // #[gpui::test] -// async fn test_copilot_multibuffer( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { +// async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); // let (copilot, copilot_lsp) = Copilot::fake(cx); // cx.update(|cx| cx.set_global(copilot)); -// let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); -// let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); -// let multibuffer = cx.add_model(|cx| { +// let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n")); +// let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n")); +// let multibuffer = cx.build_model(|cx| { // let mut multibuffer = MultiBuffer::new(0); // multibuffer.push_excerpts( // buffer_1.clone(), @@ -7373,7 +7473,7 @@ use settings::SettingsStore; // ); // multibuffer // }); -// let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); +// let editor = cx.add_window(|cx| build_editor(multibuffer, cx)); // handle_copilot_completion_request( // &copilot_lsp, @@ -7391,7 +7491,7 @@ use settings::SettingsStore; // }); // editor.next_copilot_suggestion(&Default::default(), cx); // }); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); // editor.update(cx, |editor, cx| { // assert!(editor.has_active_copilot_suggestion(cx)); // assert_eq!( @@ -7433,7 +7533,7 @@ use settings::SettingsStore; // }); // // Ensure the new suggestion is displayed when the debounce timeout expires. -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); // editor.update(cx, |editor, cx| { // assert!(editor.has_active_copilot_suggestion(cx)); // assert_eq!( @@ -7444,342 +7544,344 @@ use settings::SettingsStore; // }); // } -// #[gpui::test] -// async fn test_copilot_disabled_globs( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |settings| { -// settings -// .copilot -// .get_or_insert(Default::default()) -// .disabled_globs = Some(vec![".env*".to_string()]); -// }); - -// let (copilot, copilot_lsp) = Copilot::fake(cx); -// cx.update(|cx| cx.set_global(copilot)); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/test", -// json!({ -// ".env": "SECRET=something\n", -// "README.md": "hello\n" -// }), -// ) -// .await; -// let project = Project::test(fs, ["/test".as_ref()], cx).await; - -// let private_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/test/.env", cx) -// }) -// .await -// .unwrap(); -// let public_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/test/README.md", cx) -// }) -// .await -// .unwrap(); - -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// private_buffer.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 0), -// primary: None, -// }], -// cx, -// ); -// multibuffer.push_excerpts( -// public_buffer.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 0), -// primary: None, -// }], -// cx, -// ); -// multibuffer -// }); -// let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); - -// let mut copilot_requests = copilot_lsp -// .handle_request::(move |_params, _cx| async move { -// Ok(copilot::request::GetCompletionsResult { -// completions: vec![copilot::request::Completion { -// text: "next line".into(), -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), -// ..Default::default() -// }], -// }) -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) -// }); -// editor.next_copilot_suggestion(&Default::default(), cx); -// }); - -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// assert!(copilot_requests.try_next().is_err()); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) -// }); -// editor.next_copilot_suggestion(&Default::default(), cx); -// }); - -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// assert!(copilot_requests.try_next().is_ok()); -// } - -// #[gpui::test] -// async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// brackets: BracketPairConfig { -// pairs: vec![BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }], -// disabled_scopes_by_bracket_ix: Vec::new(), -// }, -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { -// first_trigger_character: "{".to_string(), -// more_trigger_character: None, -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { let a = 5; }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); -// let editor_handle = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// fake_server.handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 21), -// ); - -// Ok(Some(vec![lsp::TextEdit { -// new_text: "]".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), -// }])) -// }); - -// editor_handle.update(cx, |editor, cx| { -// cx.focus(&editor_handle); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(0, 21)..Point::new(0, 20)]) -// }); -// editor.handle_input("{", cx); -// }); +#[gpui::test] +async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings + .copilot + .get_or_insert(Default::default()) + .disabled_globs = Some(vec![".env*".to_string()]); + }); -// cx.foreground().run_until_parked(); + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/test", + json!({ + ".env": "SECRET=something\n", + "README.md": "hello\n" + }), + ) + .await; + let project = Project::test(fs, ["/test".as_ref()], cx).await; + + let private_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/test/.env", cx) + }) + .await + .unwrap(); + let public_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/test/README.md", cx) + }) + .await + .unwrap(); + + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + private_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + public_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer + }); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)); + + let mut copilot_requests = copilot_lsp + .handle_request::(move |_params, _cx| async move { + Ok(copilot::request::GetCompletionsResult { + completions: vec![copilot::request::Completion { + text: "next line".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), + ..Default::default() + }], + }) + }); -// buffer.read_with(cx, |buffer, _| { -// assert_eq!( -// buffer.text(), -// "fn main() { let a = {5}; }", -// "No extra braces from on type formatting should appear in the buffer" -// ) -// }); -// } + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); -// #[gpui::test] -// async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + assert!(copilot_requests.try_next().is_err()); -// let language_name: Arc = "Rust".into(); -// let mut language = Language::new( -// LanguageConfig { -// name: Arc::clone(&language_name), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); -// let server_restarts = Arc::new(AtomicUsize::new(0)); -// let closure_restarts = Arc::clone(&server_restarts); -// let language_server_name = "test language server"; -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name: language_server_name, -// initialization_options: Some(json!({ -// "testOptionValue": true -// })), -// initializer: Some(Box::new(move |fake_server| { -// let task_restarts = Arc::clone(&closure_restarts); -// fake_server.handle_request::(move |_, _| { -// task_restarts.fetch_add(1, atomic::Ordering::Release); -// futures::future::ready(Ok(())) -// }); -// })), -// ..Default::default() -// })) -// .await; + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + assert!(copilot_requests.try_next().is_ok()); +} -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { let a = 5; }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// let _fake_server = fake_servers.next().await.unwrap(); -// update_test_language_settings(cx, |language_settings| { -// language_settings.languages.insert( -// Arc::clone(&language_name), -// LanguageSettingsContent { -// tab_size: NonZeroU32::new(8), -// ..Default::default() -// }, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// server_restarts.load(atomic::Ordering::Acquire), -// 0, -// "Should not restart LSP server on an unrelated change" -// ); +#[gpui::test] +async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + brackets: BracketPairConfig { + pairs: vec![BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }], + disabled_scopes_by_bracket_ix: Vec::new(), + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: "{".to_string(), + more_trigger_character: None, + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { let a = 5; }", + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor_handle = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + + fake_server.handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 21), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "]".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), + }])) + }); -// update_test_project_settings(cx, |project_settings| { -// project_settings.lsp.insert( -// "Some other server name".into(), -// LspSettings { -// initialization_options: Some(json!({ -// "some other init value": false -// })), -// }, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// server_restarts.load(atomic::Ordering::Acquire), -// 0, -// "Should not restart LSP server on an unrelated LSP settings change" -// ); + editor_handle.update(cx, |editor, cx| { + editor.focus(cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 21)..Point::new(0, 20)]) + }); + editor.handle_input("{", cx); + }); -// update_test_project_settings(cx, |project_settings| { -// project_settings.lsp.insert( -// language_server_name.into(), -// LspSettings { -// initialization_options: Some(json!({ -// "anotherInitValue": false -// })), -// }, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// server_restarts.load(atomic::Ordering::Acquire), -// 1, -// "Should restart LSP server on a related LSP settings change" -// ); + cx.executor().run_until_parked(); -// update_test_project_settings(cx, |project_settings| { -// project_settings.lsp.insert( -// language_server_name.into(), -// LspSettings { -// initialization_options: Some(json!({ -// "anotherInitValue": false -// })), -// }, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// server_restarts.load(atomic::Ordering::Acquire), -// 1, -// "Should not restart LSP server on a related LSP settings change that is the same" -// ); + buffer.update(cx, |buffer, _| { + assert_eq!( + buffer.text(), + "fn main() { let a = {5}; }", + "No extra braces from on type formatting should appear in the buffer" + ) + }); +} -// update_test_project_settings(cx, |project_settings| { -// project_settings.lsp.insert( -// language_server_name.into(), -// LspSettings { -// initialization_options: None, -// }, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// server_restarts.load(atomic::Ordering::Acquire), -// 2, -// "Should restart LSP server on another related LSP settings change" -// ); -// } +#[gpui::test] +async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language_name: Arc = "Rust".into(); + let mut language = Language::new( + LanguageConfig { + name: Arc::clone(&language_name), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + + let server_restarts = Arc::new(AtomicUsize::new(0)); + let closure_restarts = Arc::clone(&server_restarts); + let language_server_name = "test language server"; + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: language_server_name, + initialization_options: Some(json!({ + "testOptionValue": true + })), + initializer: Some(Box::new(move |fake_server| { + let task_restarts = Arc::clone(&closure_restarts); + fake_server.handle_request::(move |_, _| { + task_restarts.fetch_add(1, atomic::Ordering::Release); + futures::future::ready(Ok(())) + }); + })), + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { let a = 5; }", + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let _fake_server = fake_servers.next().await.unwrap(); + update_test_language_settings(cx, |language_settings| { + language_settings.languages.insert( + Arc::clone(&language_name), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); + }); + cx.executor().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 0, + "Should not restart LSP server on an unrelated change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + "Some other server name".into(), + LspSettings { + initialization_options: Some(json!({ + "some other init value": false + })), + }, + ); + }); + cx.executor().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 0, + "Should not restart LSP server on an unrelated LSP settings change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: Some(json!({ + "anotherInitValue": false + })), + }, + ); + }); + cx.executor().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 1, + "Should restart LSP server on a related LSP settings change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: Some(json!({ + "anotherInitValue": false + })), + }, + ); + }); + cx.executor().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 1, + "Should not restart LSP server on a related LSP settings change that is the same" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: None, + }, + ); + }); + cx.executor().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 2, + "Should restart LSP server on another related LSP settings change" + ); +} +//todo!(completions) // #[gpui::test] // async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); @@ -7929,7 +8031,7 @@ use settings::SettingsStore; // // Trigger completion when typing a dash, because the dash is an extra // // word character in the 'element' scope, which contains the cursor. // cx.simulate_keystroke("-"); -// cx.foreground().run_until_parked(); +// cx.executor().run_until_parked(); // cx.update_editor(|editor, _| { // if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { // assert_eq!( @@ -7942,7 +8044,7 @@ use settings::SettingsStore; // }); // cx.simulate_keystroke("l"); -// cx.foreground().run_until_parked(); +// cx.executor().run_until_parked(); // cx.update_editor(|editor, _| { // if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { // assert_eq!( @@ -7958,7 +8060,7 @@ use settings::SettingsStore; // // be the start of a subword. // cx.set_state(r#"

"#); // cx.simulate_keystroke("l"); -// cx.foreground().run_until_parked(); +// cx.executor().run_until_parked(); // cx.update_editor(|editor, _| { // if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { // assert_eq!( @@ -7971,225 +8073,227 @@ use settings::SettingsStore; // }); // } -// #[gpui::test] -// async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.formatter = Some(language_settings::Formatter::Prettier) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// prettier_parser_name: Some("test_parser".to_string()), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); - -// let test_plugin = "test_plugin"; -// let _ = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// prettier_plugins: vec![test_plugin], -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_file("/file.rs", Default::default()).await; - -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; -// project.update(cx, |project, _| { -// project.languages().add(Arc::new(language)); -// }); -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) -// .await -// .unwrap(); - -// let buffer_text = "one\ntwo\nthree\n"; -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); - -// let format = editor.update(cx, |editor, cx| { -// editor.perform_format(project.clone(), FormatTrigger::Manual, cx) -// }); -// format.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// buffer_text.to_string() + prettier_format_suffix, -// "Test prettier formatting was not applied to the original buffer text", -// ); - -// update_test_language_settings(cx, |settings| { -// settings.defaults.formatter = Some(language_settings::Formatter::Auto) -// }); -// let format = editor.update(cx, |editor, cx| { -// editor.perform_format(project.clone(), FormatTrigger::Manual, cx) -// }); -// format.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, -// "Autoformatting (via test prettier) was not applied to the original buffer text", -// ); -// } - -// fn empty_range(row: usize, column: usize) -> Range { -// let point = DisplayPoint::new(row as u32, column as u32); -// point..point -// } +#[gpui::test] +async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Prettier) + }); -// fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext) { -// let (text, ranges) = marked_text_ranges(marked_text, true); -// assert_eq!(view.text(cx), text); -// assert_eq!( -// view.selections.ranges(cx), -// ranges, -// "Assert selections are {}", -// marked_text -// ); -// } + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + prettier_parser_name: Some("test_parser".to_string()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + + let test_plugin = "test_plugin"; + let _ = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + prettier_plugins: vec![test_plugin], + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.executor()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + let buffer_text = "one\ntwo\nthree\n"; + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; + editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); + + editor + .update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }) + .unwrap() + .await; + assert_eq!( + editor.update(cx, |editor, cx| editor.text(cx)), + buffer_text.to_string() + prettier_format_suffix, + "Test prettier formatting was not applied to the original buffer text", + ); + + update_test_language_settings(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Auto) + }); + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }); + format.await.unwrap(); + assert_eq!( + editor.update(cx, |editor, cx| editor.text(cx)), + buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, + "Autoformatting (via test prettier) was not applied to the original buffer text", + ); +} -// /// Handle completion request passing a marked string specifying where the completion -// /// should be triggered from using '|' character, what range should be replaced, and what completions -// /// should be returned using '<' and '>' to delimit the range -// pub fn handle_completion_request<'a>( -// cx: &mut EditorLspTestContext<'a>, -// marked_string: &str, -// completions: Vec<&'static str>, -// ) -> impl Future { -// let complete_from_marker: TextRangeMarker = '|'.into(); -// let replace_range_marker: TextRangeMarker = ('<', '>').into(); -// let (_, mut marked_ranges) = marked_text_ranges_by( -// marked_string, -// vec![complete_from_marker.clone(), replace_range_marker.clone()], -// ); +fn empty_range(row: usize, column: usize) -> Range { + let point = DisplayPoint::new(row as u32, column as u32); + point..point +} -// let complete_from_position = -// cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start); -// let replace_range = -// cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone()); +fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext) { + let (text, ranges) = marked_text_ranges(marked_text, true); + assert_eq!(view.text(cx), text); + assert_eq!( + view.selections.ranges(cx), + ranges, + "Assert selections are {}", + marked_text + ); +} -// let mut request = cx.handle_request::(move |url, params, _| { -// let completions = completions.clone(); -// async move { -// assert_eq!(params.text_document_position.text_document.uri, url.clone()); -// assert_eq!( -// params.text_document_position.position, -// complete_from_position -// ); -// Ok(Some(lsp::CompletionResponse::Array( -// completions -// .iter() -// .map(|completion_text| lsp::CompletionItem { -// label: completion_text.to_string(), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// range: replace_range, -// new_text: completion_text.to_string(), -// })), -// ..Default::default() -// }) -// .collect(), -// ))) -// } -// }); +/// Handle completion request passing a marked string specifying where the completion +/// should be triggered from using '|' character, what range should be replaced, and what completions +/// should be returned using '<' and '>' to delimit the range +pub fn handle_completion_request<'a>( + cx: &mut EditorLspTestContext<'a>, + marked_string: &str, + completions: Vec<&'static str>, +) -> impl Future { + let complete_from_marker: TextRangeMarker = '|'.into(); + let replace_range_marker: TextRangeMarker = ('<', '>').into(); + let (_, mut marked_ranges) = marked_text_ranges_by( + marked_string, + vec![complete_from_marker.clone(), replace_range_marker.clone()], + ); + + let complete_from_position = + cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start); + let replace_range = + cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone()); + + let mut request = cx.handle_request::(move |url, params, _| { + let completions = completions.clone(); + async move { + assert_eq!(params.text_document_position.text_document.uri, url.clone()); + assert_eq!( + params.text_document_position.position, + complete_from_position + ); + Ok(Some(lsp::CompletionResponse::Array( + completions + .iter() + .map(|completion_text| lsp::CompletionItem { + label: completion_text.to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: replace_range, + new_text: completion_text.to_string(), + })), + ..Default::default() + }) + .collect(), + ))) + } + }); -// async move { -// request.next().await; -// } -// } + async move { + request.next().await; + } +} -// fn handle_resolve_completion_request<'a>( -// cx: &mut EditorLspTestContext<'a>, -// edits: Option>, -// ) -> impl Future { -// let edits = edits.map(|edits| { -// edits -// .iter() -// .map(|(marked_string, new_text)| { -// let (_, marked_ranges) = marked_text_ranges(marked_string, false); -// let replace_range = cx.to_lsp_range(marked_ranges[0].clone()); -// lsp::TextEdit::new(replace_range, new_text.to_string()) -// }) -// .collect::>() -// }); +fn handle_resolve_completion_request<'a>( + cx: &mut EditorLspTestContext<'a>, + edits: Option>, +) -> impl Future { + let edits = edits.map(|edits| { + edits + .iter() + .map(|(marked_string, new_text)| { + let (_, marked_ranges) = marked_text_ranges(marked_string, false); + let replace_range = cx.to_lsp_range(marked_ranges[0].clone()); + lsp::TextEdit::new(replace_range, new_text.to_string()) + }) + .collect::>() + }); -// let mut request = -// cx.handle_request::(move |_, _, _| { -// let edits = edits.clone(); -// async move { -// Ok(lsp::CompletionItem { -// additional_text_edits: edits, -// ..Default::default() -// }) -// } -// }); + let mut request = + cx.handle_request::(move |_, _, _| { + let edits = edits.clone(); + async move { + Ok(lsp::CompletionItem { + additional_text_edits: edits, + ..Default::default() + }) + } + }); -// async move { -// request.next().await; -// } -// } + async move { + request.next().await; + } +} -// fn handle_copilot_completion_request( -// lsp: &lsp::FakeLanguageServer, -// completions: Vec, -// completions_cycling: Vec, -// ) { -// lsp.handle_request::(move |_params, _cx| { -// let completions = completions.clone(); -// async move { -// Ok(copilot::request::GetCompletionsResult { -// completions: completions.clone(), -// }) -// } -// }); -// lsp.handle_request::(move |_params, _cx| { -// let completions_cycling = completions_cycling.clone(); -// async move { -// Ok(copilot::request::GetCompletionsResult { -// completions: completions_cycling.clone(), -// }) -// } -// }); -// } +fn handle_copilot_completion_request( + lsp: &lsp::FakeLanguageServer, + completions: Vec, + completions_cycling: Vec, +) { + lsp.handle_request::(move |_params, _cx| { + let completions = completions.clone(); + async move { + Ok(copilot::request::GetCompletionsResult { + completions: completions.clone(), + }) + } + }); + lsp.handle_request::(move |_params, _cx| { + let completions_cycling = completions_cycling.clone(); + async move { + Ok(copilot::request::GetCompletionsResult { + completions: completions_cycling.clone(), + }) + } + }); +} pub(crate) fn update_test_language_settings( cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent), ) { cx.update(|cx| { - cx.update_global::(|store, cx| { + cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, f); }); }); } -// pub(crate) fn update_test_project_settings( -// cx: &mut TestAppContext, -// f: impl Fn(&mut ProjectSettings), -// ) { -// cx.update(|cx| { -// cx.update_global::(|store, cx| { -// store.update_user_settings::(cx, f); -// }); -// }); -// } - -// pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) { -// cx.foreground().forbid_parking(); +pub(crate) fn update_test_project_settings( + cx: &mut TestAppContext, + f: impl Fn(&mut ProjectSettings), +) { + cx.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, f); + }); + }); +} -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init((), cx); -// client::init_settings(cx); -// language::init(cx); -// Project::init_settings(cx); -// workspace::init_settings(cx); -// crate::init(cx); -// }); +pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) { + cx.update(|cx| { + let store = SettingsStore::test(cx); + cx.set_global(store); + theme::init(cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + crate::init(cx); + }); -// update_test_language_settings(cx, f); -// } + update_test_language_settings(cx, f); +} diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index ba2dcb431a6ccf68b5da662c4fc31c0028d43fd8..dd834b4cd89998211b7290b98db8d5323fe55878 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1683,21 +1683,24 @@ impl EditorElement { ShowScrollbar::Never => false, }; - let fold_ranges: Vec<(BufferRow, Range, Hsla)> = fold_ranges - .into_iter() - .map(|(id, fold)| { - todo!("folds!") - // let color = self - // .style - // .folds - // .ellipses - // .background - // .style_for(&mut cx.mouse_state::(id as usize)) - // .color; - - // (id, fold, color) - }) - .collect(); + let fold_ranges: Vec<(BufferRow, Range, Hsla)> = Vec::new(); + // todo!() + + // fold_ranges + // .into_iter() + // .map(|(id, fold)| { + // // todo!("folds!") + // // let color = self + // // .style + // // .folds + // // .ellipses + // // .background + // // .style_for(&mut cx.mouse_state::(id as usize)) + // // .color; + + // // (id, fold, color) + // }) + // .collect(); let head_for_relative = newest_selection_head.unwrap_or_else(|| { let newest = editor.selections.newest::(cx); diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index b1fc69ecac8cbd427ce3696d3c97bddee15ef980..4b43a63f0582d948da40301a27f07df21b7b0b52 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -30,6 +30,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; +use ui::{Label, LabelColor}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::{ @@ -595,16 +596,19 @@ impl Item for Editor { .flex_row() .items_center() .gap_2() - .child(self.title(cx).to_string()) + .child(Label::new(self.title(cx).to_string())) .children(detail.and_then(|detail| { let path = path_for_buffer(&self.buffer, detail, false, cx)?; let description = path.to_string_lossy(); Some( - div() - .text_color(theme.colors().text_muted) - .text_xs() - .child(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)), + div().child( + Label::new(util::truncate_and_trailoff( + &description, + MAX_TAB_TITLE_LEN, + )) + .color(LabelColor::Muted), + ), ) })), ) diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index 23c5a75809719fc918a8716d17e949538fcc1c2c..01e241c830d2d71edf74eb4a7cabfca0e175f12d 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -315,11 +315,14 @@ impl SelectionsCollection { let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details); + dbg!("****START COL****"); let start_col = layed_out_line.closest_index_for_x(positions.start) as u32; if start_col < line_len || (is_empty && positions.start == layed_out_line.width) { let start = DisplayPoint::new(row, start_col); + dbg!("****END COL****"); let end_col = layed_out_line.closest_index_for_x(positions.end) as u32; let end = DisplayPoint::new(row, end_col); + dbg!(start_col, end_col); Some(Selection { id: post_inc(&mut self.next_selection_id), diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index 14619c4a98237cff93595b6ba52abee033babbcc..ec37c57f2c9656a978e289cbb33f8580004f98f9 100644 --- a/crates/editor2/src/test.rs +++ b/crates/editor2/src/test.rs @@ -1,81 +1,74 @@ pub mod editor_lsp_test_context; pub mod editor_test_context; -// todo!() -// use crate::{ -// display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, -// DisplayPoint, Editor, EditorMode, MultiBuffer, -// }; +use crate::{ + display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, + DisplayPoint, Editor, EditorMode, MultiBuffer, +}; -// use gpui::{Model, ViewContext}; +use gpui::{Context, Model, Pixels, ViewContext}; -// use project::Project; -// use util::test::{marked_text_offsets, marked_text_ranges}; +use project::Project; +use util::test::{marked_text_offsets, marked_text_ranges}; -// #[cfg(test)] -// #[ctor::ctor] -// fn init_logger() { -// if std::env::var("RUST_LOG").is_ok() { -// env_logger::init(); -// } -// } +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} -// // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. -// pub fn marked_display_snapshot( -// text: &str, -// cx: &mut gpui::AppContext, -// ) -> (DisplaySnapshot, Vec) { -// let (unmarked_text, markers) = marked_text_offsets(text); +// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. +pub fn marked_display_snapshot( + text: &str, + cx: &mut gpui::AppContext, +) -> (DisplaySnapshot, Vec) { + let (unmarked_text, markers) = marked_text_offsets(text); -// let family_id = cx -// .font_cache() -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = cx -// .font_cache() -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 14.0; + let font = cx.text_style().font(); + let font_size: Pixels = 14.into(); -// let buffer = MultiBuffer::build_simple(&unmarked_text, cx); -// let display_map = -// cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); -// let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); -// let markers = markers -// .into_iter() -// .map(|offset| offset.to_display_point(&snapshot)) -// .collect(); + let buffer = MultiBuffer::build_simple(&unmarked_text, cx); + let display_map = cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx)); + let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + let markers = markers + .into_iter() + .map(|offset| offset.to_display_point(&snapshot)) + .collect(); -// (snapshot, markers) -// } + (snapshot, markers) +} -// pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext) { -// let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); -// assert_eq!(editor.text(cx), unmarked_text); -// editor.change_selections(None, cx, |s| s.select_ranges(text_ranges)); -// } +pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext) { + let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); + assert_eq!(editor.text(cx), unmarked_text); + editor.change_selections(None, cx, |s| s.select_ranges(text_ranges)); +} -// pub fn assert_text_with_selections( -// editor: &mut Editor, -// marked_text: &str, -// cx: &mut ViewContext, -// ) { -// let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); -// assert_eq!(editor.text(cx), unmarked_text); -// assert_eq!(editor.selections.ranges(cx), text_ranges); -// } +pub fn assert_text_with_selections( + editor: &mut Editor, + marked_text: &str, + cx: &mut ViewContext, +) { + let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); + assert_eq!(editor.text(cx), unmarked_text); + assert_eq!(editor.selections.ranges(cx), text_ranges); +} -// // RA thinks this is dead code even though it is used in a whole lot of tests -// #[allow(dead_code)] -// #[cfg(any(test, feature = "test-support"))] -// pub(crate) fn build_editor(buffer: Model, cx: &mut ViewContext) -> Editor { -// Editor::new(EditorMode::Full, buffer, None, None, cx) -// } +// RA thinks this is dead code even though it is used in a whole lot of tests +#[allow(dead_code)] +#[cfg(any(test, feature = "test-support"))] +pub(crate) fn build_editor(buffer: Model, cx: &mut ViewContext) -> Editor { + // todo!() + Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx) +} -// pub(crate) fn build_editor_with_project( -// project: Model, -// buffer: Model, -// cx: &mut ViewContext, -// ) -> Editor { -// Editor::new(EditorMode::Full, buffer, Some(project), None, cx) -// } +pub(crate) fn build_editor_with_project( + project: Model, + buffer: Model, + cx: &mut ViewContext, +) -> Editor { + // todo!() + Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx) +} diff --git a/crates/editor2/src/test/editor_lsp_test_context.rs b/crates/editor2/src/test/editor_lsp_test_context.rs index 5a6bdd2723d192486b2fddf4d25cbe3d6818e0af..7ee55cddba1edba9356be2c6773c3f097f57c1c8 100644 --- a/crates/editor2/src/test/editor_lsp_test_context.rs +++ b/crates/editor2/src/test/editor_lsp_test_context.rs @@ -1,297 +1,298 @@ -// use std::{ -// borrow::Cow, -// ops::{Deref, DerefMut, Range}, -// sync::Arc, -// }; - -// use anyhow::Result; - -// use crate::{Editor, ToPoint}; -// use collections::HashSet; -// use futures::Future; -// use gpui::{json, View, ViewContext}; -// use indoc::indoc; -// use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; -// use lsp::{notification, request}; -// use multi_buffer::ToPointUtf16; -// use project::Project; -// use smol::stream::StreamExt; -// use workspace::{AppState, Workspace, WorkspaceHandle}; - -// use super::editor_test_context::EditorTestContext; - -// pub struct EditorLspTestContext<'a> { -// pub cx: EditorTestContext<'a>, -// pub lsp: lsp::FakeLanguageServer, -// pub workspace: View, -// pub buffer_lsp_url: lsp::Url, -// } - -// impl<'a> EditorLspTestContext<'a> { -// pub async fn new( -// mut language: Language, -// capabilities: lsp::ServerCapabilities, -// cx: &'a mut gpui::TestAppContext, -// ) -> EditorLspTestContext<'a> { -// use json::json; - -// let app_state = cx.update(AppState::test); - -// cx.update(|cx| { -// language::init(cx); -// crate::init(cx); -// workspace::init(app_state.clone(), cx); -// Project::init_settings(cx); -// }); - -// let file_name = format!( -// "file.{}", -// language -// .path_suffixes() -// .first() -// .expect("language must have a path suffix for EditorLspTestContext") -// ); - -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities, -// ..Default::default() -// })) -// .await; - -// let project = Project::test(app_state.fs.clone(), [], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); - -// app_state -// .fs -// .as_fake() -// .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }})) -// .await; - -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// project -// .update(cx, |project, cx| { -// project.find_or_create_local_worktree("/root", true, cx) -// }) -// .await -// .unwrap(); -// cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) -// .await; - -// let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); -// let item = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path(file, None, true, cx) -// }) -// .await -// .expect("Could not open test file"); - -// let editor = cx.update(|cx| { -// item.act_as::(cx) -// .expect("Opened test file wasn't an editor") -// }); -// editor.update(cx, |_, cx| cx.focus_self()); - -// let lsp = fake_servers.next().await.unwrap(); - -// Self { -// cx: EditorTestContext { -// cx, -// window: window.into(), -// editor, -// }, -// lsp, -// workspace, -// buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(), -// } -// } - -// pub async fn new_rust( -// capabilities: lsp::ServerCapabilities, -// cx: &'a mut gpui::TestAppContext, -// ) -> EditorLspTestContext<'a> { -// let language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_queries(LanguageQueries { -// indents: Some(Cow::from(indoc! {r#" -// [ -// ((where_clause) _ @end) -// (field_expression) -// (call_expression) -// (assignment_expression) -// (let_declaration) -// (let_chain) -// (await_expression) -// ] @indent - -// (_ "[" "]" @end) @indent -// (_ "<" ">" @end) @indent -// (_ "{" "}" @end) @indent -// (_ "(" ")" @end) @indent"#})), -// brackets: Some(Cow::from(indoc! {r#" -// ("(" @open ")" @close) -// ("[" @open "]" @close) -// ("{" @open "}" @close) -// ("<" @open ">" @close) -// ("\"" @open "\"" @close) -// (closure_parameters "|" @open "|" @close)"#})), -// ..Default::default() -// }) -// .expect("Could not parse queries"); - -// Self::new(language, capabilities, cx).await -// } - -// pub async fn new_typescript( -// capabilities: lsp::ServerCapabilities, -// cx: &'a mut gpui::TestAppContext, -// ) -> EditorLspTestContext<'a> { -// let mut word_characters: HashSet = Default::default(); -// word_characters.insert('$'); -// word_characters.insert('#'); -// let language = Language::new( -// LanguageConfig { -// name: "Typescript".into(), -// path_suffixes: vec!["ts".to_string()], -// brackets: language::BracketPairConfig { -// pairs: vec![language::BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }], -// disabled_scopes_by_bracket_ix: Default::default(), -// }, -// word_characters, -// ..Default::default() -// }, -// Some(tree_sitter_typescript::language_typescript()), -// ) -// .with_queries(LanguageQueries { -// brackets: Some(Cow::from(indoc! {r#" -// ("(" @open ")" @close) -// ("[" @open "]" @close) -// ("{" @open "}" @close) -// ("<" @open ">" @close) -// ("\"" @open "\"" @close)"#})), -// indents: Some(Cow::from(indoc! {r#" -// [ -// (call_expression) -// (assignment_expression) -// (member_expression) -// (lexical_declaration) -// (variable_declaration) -// (assignment_expression) -// (if_statement) -// (for_statement) -// ] @indent - -// (_ "[" "]" @end) @indent -// (_ "<" ">" @end) @indent -// (_ "{" "}" @end) @indent -// (_ "(" ")" @end) @indent -// "#})), -// ..Default::default() -// }) -// .expect("Could not parse queries"); - -// Self::new(language, capabilities, cx).await -// } - -// // Constructs lsp range using a marked string with '[', ']' range delimiters -// pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { -// let ranges = self.ranges(marked_text); -// self.to_lsp_range(ranges[0].clone()) -// } - -// pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { -// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); -// let start_point = range.start.to_point(&snapshot.buffer_snapshot); -// let end_point = range.end.to_point(&snapshot.buffer_snapshot); - -// self.editor(|editor, cx| { -// let buffer = editor.buffer().read(cx); -// let start = point_to_lsp( -// buffer -// .point_to_buffer_offset(start_point, cx) -// .unwrap() -// .1 -// .to_point_utf16(&buffer.read(cx)), -// ); -// let end = point_to_lsp( -// buffer -// .point_to_buffer_offset(end_point, cx) -// .unwrap() -// .1 -// .to_point_utf16(&buffer.read(cx)), -// ); - -// lsp::Range { start, end } -// }) -// } - -// pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { -// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); -// let point = offset.to_point(&snapshot.buffer_snapshot); - -// self.editor(|editor, cx| { -// let buffer = editor.buffer().read(cx); -// point_to_lsp( -// buffer -// .point_to_buffer_offset(point, cx) -// .unwrap() -// .1 -// .to_point_utf16(&buffer.read(cx)), -// ) -// }) -// } - -// pub fn update_workspace(&mut self, update: F) -> T -// where -// F: FnOnce(&mut Workspace, &mut ViewContext) -> T, -// { -// self.workspace.update(self.cx.cx, update) -// } - -// pub fn handle_request( -// &self, -// mut handler: F, -// ) -> futures::channel::mpsc::UnboundedReceiver<()> -// where -// T: 'static + request::Request, -// T::Params: 'static + Send, -// F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut, -// Fut: 'static + Send + Future>, -// { -// let url = self.buffer_lsp_url.clone(); -// self.lsp.handle_request::(move |params, cx| { -// let url = url.clone(); -// handler(url, params, cx) -// }) -// } - -// pub fn notify(&self, params: T::Params) { -// self.lsp.notify::(params); -// } -// } - -// impl<'a> Deref for EditorLspTestContext<'a> { -// type Target = EditorTestContext<'a>; - -// fn deref(&self) -> &Self::Target { -// &self.cx -// } -// } - -// impl<'a> DerefMut for EditorLspTestContext<'a> { -// fn deref_mut(&mut self) -> &mut Self::Target { -// &mut self.cx -// } -// } +use std::{ + borrow::Cow, + ops::{Deref, DerefMut, Range}, + sync::Arc, +}; + +use anyhow::Result; +use serde_json::json; + +use crate::{Editor, ToPoint}; +use collections::HashSet; +use futures::Future; +use gpui::{View, ViewContext, VisualTestContext}; +use indoc::indoc; +use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; +use lsp::{notification, request}; +use multi_buffer::ToPointUtf16; +use project::Project; +use smol::stream::StreamExt; +use workspace::{AppState, Workspace, WorkspaceHandle}; + +use super::editor_test_context::{AssertionContextManager, EditorTestContext}; + +pub struct EditorLspTestContext<'a> { + pub cx: EditorTestContext<'a>, + pub lsp: lsp::FakeLanguageServer, + pub workspace: View, + pub buffer_lsp_url: lsp::Url, +} + +impl<'a> EditorLspTestContext<'a> { + pub async fn new( + mut language: Language, + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + let app_state = cx.update(AppState::test); + + cx.update(|cx| { + language::init(cx); + crate::init(cx); + workspace::init(app_state.clone(), cx); + Project::init_settings(cx); + }); + + let file_name = format!( + "file.{}", + language + .path_suffixes() + .first() + .expect("language must have a path suffix for EditorLspTestContext") + ); + + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities, + ..Default::default() + })) + .await; + + let project = Project::test(app_state.fs.clone(), [], cx).await; + + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + + app_state + .fs + .as_fake() + .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }})) + .await; + + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + + let workspace = window.root_view(cx).unwrap(); + + let mut cx = VisualTestContext::from_window(*window.deref(), cx); + project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree("/root", true, cx) + }) + .await + .unwrap(); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); + let item = workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path(file, None, true, cx) + }) + .await + .expect("Could not open test file"); + let editor = cx.update(|cx| { + item.act_as::(cx) + .expect("Opened test file wasn't an editor") + }); + editor.update(&mut cx, |editor, cx| editor.focus(cx)); + + let lsp = fake_servers.next().await.unwrap(); + Self { + cx: EditorTestContext { + cx, + window: window.into(), + editor, + assertion_cx: AssertionContextManager::new(), + }, + lsp, + workspace, + buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(), + } + } + + pub async fn new_rust( + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_queries(LanguageQueries { + indents: Some(Cow::from(indoc! {r#" + [ + ((where_clause) _ @end) + (field_expression) + (call_expression) + (assignment_expression) + (let_declaration) + (let_chain) + (await_expression) + ] @indent + + (_ "[" "]" @end) @indent + (_ "<" ">" @end) @indent + (_ "{" "}" @end) @indent + (_ "(" ")" @end) @indent"#})), + brackets: Some(Cow::from(indoc! {r#" + ("(" @open ")" @close) + ("[" @open "]" @close) + ("{" @open "}" @close) + ("<" @open ">" @close) + ("\"" @open "\"" @close) + (closure_parameters "|" @open "|" @close)"#})), + ..Default::default() + }) + .expect("Could not parse queries"); + + Self::new(language, capabilities, cx).await + } + + pub async fn new_typescript( + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + let mut word_characters: HashSet = Default::default(); + word_characters.insert('$'); + word_characters.insert('#'); + let language = Language::new( + LanguageConfig { + name: "Typescript".into(), + path_suffixes: vec!["ts".to_string()], + brackets: language::BracketPairConfig { + pairs: vec![language::BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }], + disabled_scopes_by_bracket_ix: Default::default(), + }, + word_characters, + ..Default::default() + }, + Some(tree_sitter_typescript::language_typescript()), + ) + .with_queries(LanguageQueries { + brackets: Some(Cow::from(indoc! {r#" + ("(" @open ")" @close) + ("[" @open "]" @close) + ("{" @open "}" @close) + ("<" @open ">" @close) + ("\"" @open "\"" @close)"#})), + indents: Some(Cow::from(indoc! {r#" + [ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) + ] @indent + + (_ "[" "]" @end) @indent + (_ "<" ">" @end) @indent + (_ "{" "}" @end) @indent + (_ "(" ")" @end) @indent + "#})), + ..Default::default() + }) + .expect("Could not parse queries"); + + Self::new(language, capabilities, cx).await + } + + // Constructs lsp range using a marked string with '[', ']' range delimiters + pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { + let ranges = self.ranges(marked_text); + self.to_lsp_range(ranges[0].clone()) + } + + pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let start_point = range.start.to_point(&snapshot.buffer_snapshot); + let end_point = range.end.to_point(&snapshot.buffer_snapshot); + + self.editor(|editor, cx| { + let buffer = editor.buffer().read(cx); + let start = point_to_lsp( + buffer + .point_to_buffer_offset(start_point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ); + let end = point_to_lsp( + buffer + .point_to_buffer_offset(end_point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ); + + lsp::Range { start, end } + }) + } + + pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let point = offset.to_point(&snapshot.buffer_snapshot); + + self.editor(|editor, cx| { + let buffer = editor.buffer().read(cx); + point_to_lsp( + buffer + .point_to_buffer_offset(point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ) + }) + } + + pub fn update_workspace(&mut self, update: F) -> T + where + F: FnOnce(&mut Workspace, &mut ViewContext) -> T, + { + self.workspace.update(&mut self.cx.cx, update) + } + + pub fn handle_request( + &self, + mut handler: F, + ) -> futures::channel::mpsc::UnboundedReceiver<()> + where + T: 'static + request::Request, + T::Params: 'static + Send, + F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut, + Fut: 'static + Send + Future>, + { + let url = self.buffer_lsp_url.clone(); + self.lsp.handle_request::(move |params, cx| { + let url = url.clone(); + handler(url, params, cx) + }) + } + + pub fn notify(&self, params: T::Params) { + self.lsp.notify::(params); + } +} + +impl<'a> Deref for EditorLspTestContext<'a> { + type Target = EditorTestContext<'a>; + + fn deref(&self) -> &Self::Target { + &self.cx + } +} + +impl<'a> DerefMut for EditorLspTestContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx + } +} diff --git a/crates/editor2/src/test/editor_test_context.rs b/crates/editor2/src/test/editor_test_context.rs index 4bf32d061323b9fc6e196dcf7decb1b50777e973..c865538b0c5cf3876ea1e955ac4d4eb64d6a2b67 100644 --- a/crates/editor2/src/test/editor_test_context.rs +++ b/crates/editor2/src/test/editor_test_context.rs @@ -1,331 +1,400 @@ use crate::{ display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, }; +use collections::BTreeMap; use futures::Future; use gpui::{ AnyWindowHandle, AppContext, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext, + VisualTestContext, WindowHandle, }; use indoc::indoc; +use itertools::Itertools; use language::{Buffer, BufferSnapshot}; +use parking_lot::RwLock; use project::{FakeFs, Project}; use std::{ any::TypeId, ops::{Deref, DerefMut, Range}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, }; use util::{ assert_set_eq, test::{generate_marked_text, marked_text_ranges}, }; -// use super::build_editor_with_project; - -// pub struct EditorTestContext<'a> { -// pub cx: &'a mut gpui::TestAppContext, -// pub window: AnyWindowHandle, -// pub editor: View, -// } - -// impl<'a> EditorTestContext<'a> { -// pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { -// let fs = FakeFs::new(cx.background()); -// // fs.insert_file("/file", "".to_owned()).await; -// fs.insert_tree( -// "/root", -// gpui::serde_json::json!({ -// "file": "", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/root".as_ref()], cx).await; -// let buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/root/file", cx) -// }) -// .await -// .unwrap(); -// let window = cx.add_window(|cx| { -// cx.focus_self(); -// build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx) -// }); -// let editor = window.root(cx); -// Self { -// cx, -// window: window.into(), -// editor, -// } -// } - -// pub fn condition( -// &self, -// predicate: impl FnMut(&Editor, &AppContext) -> bool, -// ) -> impl Future { -// self.editor.condition(self.cx, predicate) -// } - -// pub fn editor(&self, read: F) -> T -// where -// F: FnOnce(&Editor, &ViewContext) -> T, -// { -// self.editor.update(self.cx, read) -// } - -// pub fn update_editor(&mut self, update: F) -> T -// where -// F: FnOnce(&mut Editor, &mut ViewContext) -> T, -// { -// self.editor.update(self.cx, update) -// } - -// pub fn multibuffer(&self, read: F) -> T -// where -// F: FnOnce(&MultiBuffer, &AppContext) -> T, -// { -// self.editor(|editor, cx| read(editor.buffer().read(cx), cx)) -// } - -// pub fn update_multibuffer(&mut self, update: F) -> T -// where -// F: FnOnce(&mut MultiBuffer, &mut ModelContext) -> T, -// { -// self.update_editor(|editor, cx| editor.buffer().update(cx, update)) -// } - -// pub fn buffer_text(&self) -> String { -// self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) -// } - -// pub fn buffer(&self, read: F) -> T -// where -// F: FnOnce(&Buffer, &AppContext) -> T, -// { -// self.multibuffer(|multibuffer, cx| { -// let buffer = multibuffer.as_singleton().unwrap().read(cx); -// read(buffer, cx) -// }) -// } - -// pub fn update_buffer(&mut self, update: F) -> T -// where -// F: FnOnce(&mut Buffer, &mut ModelContext) -> T, -// { -// self.update_multibuffer(|multibuffer, cx| { -// let buffer = multibuffer.as_singleton().unwrap(); -// buffer.update(cx, update) -// }) -// } - -// pub fn buffer_snapshot(&self) -> BufferSnapshot { -// self.buffer(|buffer, _| buffer.snapshot()) -// } - -// pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle { -// let keystroke_under_test_handle = -// self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); -// let keystroke = Keystroke::parse(keystroke_text).unwrap(); - -// self.cx.dispatch_keystroke(self.window, keystroke, false); - -// keystroke_under_test_handle -// } - -// pub fn simulate_keystrokes( -// &mut self, -// keystroke_texts: [&str; COUNT], -// ) -> ContextHandle { -// let keystrokes_under_test_handle = -// self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts)); -// for keystroke_text in keystroke_texts.into_iter() { -// self.simulate_keystroke(keystroke_text); -// } -// // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete -// // before returning. -// // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too -// // quickly races with async actions. -// if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() { -// executor.run_until_parked(); -// } else { -// unreachable!(); -// } - -// keystrokes_under_test_handle -// } - -// pub fn ranges(&self, marked_text: &str) -> Vec> { -// let (unmarked_text, ranges) = marked_text_ranges(marked_text, false); -// assert_eq!(self.buffer_text(), unmarked_text); -// ranges -// } - -// pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint { -// let ranges = self.ranges(marked_text); -// let snapshot = self -// .editor -// .update(self.cx, |editor, cx| editor.snapshot(cx)); -// ranges[0].start.to_display_point(&snapshot) -// } - -// // Returns anchors for the current buffer using `«` and `»` -// pub fn text_anchor_range(&self, marked_text: &str) -> Range { -// let ranges = self.ranges(marked_text); -// let snapshot = self.buffer_snapshot(); -// snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) -// } - -// pub fn set_diff_base(&mut self, diff_base: Option<&str>) { -// let diff_base = diff_base.map(String::from); -// self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx)); -// } - -// /// Change the editor's text and selections using a string containing -// /// embedded range markers that represent the ranges and directions of -// /// each selection. -// /// -// /// Returns a context handle so that assertion failures can print what -// /// editor state was needed to cause the failure. -// /// -// /// See the `util::test::marked_text_ranges` function for more information. -// pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { -// let state_context = self.add_assertion_context(format!( -// "Initial Editor State: \"{}\"", -// marked_text.escape_debug().to_string() -// )); -// let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); -// self.editor.update(self.cx, |editor, cx| { -// editor.set_text(unmarked_text, cx); -// editor.change_selections(Some(Autoscroll::fit()), cx, |s| { -// s.select_ranges(selection_ranges) -// }) -// }); -// state_context -// } - -// /// Only change the editor's selections -// pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle { -// let state_context = self.add_assertion_context(format!( -// "Initial Editor State: \"{}\"", -// marked_text.escape_debug().to_string() -// )); -// let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); -// self.editor.update(self.cx, |editor, cx| { -// assert_eq!(editor.text(cx), unmarked_text); -// editor.change_selections(Some(Autoscroll::fit()), cx, |s| { -// s.select_ranges(selection_ranges) -// }) -// }); -// state_context -// } - -// /// Make an assertion about the editor's text and the ranges and directions -// /// of its selections using a string containing embedded range markers. -// /// -// /// See the `util::test::marked_text_ranges` function for more information. -// #[track_caller] -// pub fn assert_editor_state(&mut self, marked_text: &str) { -// let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true); -// let buffer_text = self.buffer_text(); - -// if buffer_text != unmarked_text { -// panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}"); -// } - -// self.assert_selections(expected_selections, marked_text.to_string()) -// } - -// pub fn editor_state(&mut self) -> String { -// generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true) -// } - -// #[track_caller] -// pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { -// let expected_ranges = self.ranges(marked_text); -// let actual_ranges: Vec> = self.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// editor -// .background_highlights -// .get(&TypeId::of::()) -// .map(|h| h.1.clone()) -// .unwrap_or_default() -// .into_iter() -// .map(|range| range.to_offset(&snapshot.buffer_snapshot)) -// .collect() -// }); -// assert_set_eq!(actual_ranges, expected_ranges); -// } - -// #[track_caller] -// pub fn assert_editor_text_highlights(&mut self, marked_text: &str) { -// let expected_ranges = self.ranges(marked_text); -// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); -// let actual_ranges: Vec> = snapshot -// .text_highlight_ranges::() -// .map(|ranges| ranges.as_ref().clone().1) -// .unwrap_or_default() -// .into_iter() -// .map(|range| range.to_offset(&snapshot.buffer_snapshot)) -// .collect(); -// assert_set_eq!(actual_ranges, expected_ranges); -// } - -// #[track_caller] -// pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { -// let expected_marked_text = -// generate_marked_text(&self.buffer_text(), &expected_selections, true); -// self.assert_selections(expected_selections, expected_marked_text) -// } - -// fn editor_selections(&self) -> Vec> { -// self.editor -// .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) -// .into_iter() -// .map(|s| { -// if s.reversed { -// s.end..s.start -// } else { -// s.start..s.end -// } -// }) -// .collect::>() -// } - -// #[track_caller] -// fn assert_selections( -// &mut self, -// expected_selections: Vec>, -// expected_marked_text: String, -// ) { -// let actual_selections = self.editor_selections(); -// let actual_marked_text = -// generate_marked_text(&self.buffer_text(), &actual_selections, true); -// if expected_selections != actual_selections { -// panic!( -// indoc! {" - -// {}Editor has unexpected selections. - -// Expected selections: -// {} - -// Actual selections: -// {} -// "}, -// self.assertion_context(), -// expected_marked_text, -// actual_marked_text, -// ); -// } -// } -// } -// -// impl<'a> Deref for EditorTestContext<'a> { -// type Target = gpui::TestAppContext; - -// fn deref(&self) -> &Self::Target { -// self.cx -// } -// } - -// impl<'a> DerefMut for EditorTestContext<'a> { -// fn deref_mut(&mut self) -> &mut Self::Target { -// &mut self.cx -// } -// } +use super::build_editor_with_project; + +pub struct EditorTestContext<'a> { + pub cx: gpui::VisualTestContext<'a>, + pub window: AnyWindowHandle, + pub editor: View, + pub assertion_cx: AssertionContextManager, +} + +impl<'a> EditorTestContext<'a> { + pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { + let fs = FakeFs::new(cx.executor()); + // fs.insert_file("/file", "".to_owned()).await; + fs.insert_tree( + "/root", + gpui::serde_json::json!({ + "file": "", + }), + ) + .await; + let project = Project::test(fs, ["/root".as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/root/file", cx) + }) + .await + .unwrap(); + let editor = cx.add_window(|cx| { + let editor = + build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx); + editor.focus(cx); + editor + }); + let editor_view = editor.root_view(cx).unwrap(); + Self { + cx: VisualTestContext::from_window(*editor.deref(), cx), + window: editor.into(), + editor: editor_view, + assertion_cx: AssertionContextManager::new(), + } + } + + pub fn condition( + &self, + predicate: impl FnMut(&Editor, &AppContext) -> bool, + ) -> impl Future { + self.editor.condition::(&self.cx, predicate) + } + + #[track_caller] + pub fn editor(&mut self, read: F) -> T + where + F: FnOnce(&Editor, &ViewContext) -> T, + { + self.editor + .update(&mut self.cx, |this, cx| read(&this, &cx)) + } + + #[track_caller] + pub fn update_editor(&mut self, update: F) -> T + where + F: FnOnce(&mut Editor, &mut ViewContext) -> T, + { + self.editor.update(&mut self.cx, update) + } + + pub fn multibuffer(&mut self, read: F) -> T + where + F: FnOnce(&MultiBuffer, &AppContext) -> T, + { + self.editor(|editor, cx| read(editor.buffer().read(cx), cx)) + } + + pub fn update_multibuffer(&mut self, update: F) -> T + where + F: FnOnce(&mut MultiBuffer, &mut ModelContext) -> T, + { + self.update_editor(|editor, cx| editor.buffer().update(cx, update)) + } + + pub fn buffer_text(&mut self) -> String { + self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) + } + + pub fn buffer(&mut self, read: F) -> T + where + F: FnOnce(&Buffer, &AppContext) -> T, + { + self.multibuffer(|multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap().read(cx); + read(buffer, cx) + }) + } + + pub fn update_buffer(&mut self, update: F) -> T + where + F: FnOnce(&mut Buffer, &mut ModelContext) -> T, + { + self.update_multibuffer(|multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap(); + buffer.update(cx, update) + }) + } + + pub fn buffer_snapshot(&mut self) -> BufferSnapshot { + self.buffer(|buffer, _| buffer.snapshot()) + } + + pub fn add_assertion_context(&self, context: String) -> ContextHandle { + self.assertion_cx.add_context(context) + } + + pub fn assertion_context(&self) -> String { + self.assertion_cx.context() + } + + pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle { + let keystroke_under_test_handle = + self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); + let keystroke = Keystroke::parse(keystroke_text).unwrap(); + + self.cx.dispatch_keystroke(self.window, keystroke, false); + + keystroke_under_test_handle + } + + pub fn simulate_keystrokes( + &mut self, + keystroke_texts: [&str; COUNT], + ) -> ContextHandle { + let keystrokes_under_test_handle = + self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts)); + for keystroke_text in keystroke_texts.into_iter() { + self.simulate_keystroke(keystroke_text); + } + // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete + // before returning. + // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too + // quickly races with async actions. + self.cx.background_executor.run_until_parked(); + + keystrokes_under_test_handle + } + + pub fn ranges(&mut self, marked_text: &str) -> Vec> { + let (unmarked_text, ranges) = marked_text_ranges(marked_text, false); + assert_eq!(self.buffer_text(), unmarked_text); + ranges + } + + pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint { + let ranges = self.ranges(marked_text); + let snapshot = self + .editor + .update(&mut self.cx, |editor, cx| editor.snapshot(cx)); + ranges[0].start.to_display_point(&snapshot) + } + + // Returns anchors for the current buffer using `«` and `»` + pub fn text_anchor_range(&mut self, marked_text: &str) -> Range { + let ranges = self.ranges(marked_text); + let snapshot = self.buffer_snapshot(); + snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) + } + + pub fn set_diff_base(&mut self, diff_base: Option<&str>) { + let diff_base = diff_base.map(String::from); + self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx)); + } + + /// Change the editor's text and selections using a string containing + /// embedded range markers that represent the ranges and directions of + /// each selection. + /// + /// Returns a context handle so that assertion failures can print what + /// editor state was needed to cause the failure. + /// + /// See the `util::test::marked_text_ranges` function for more information. + pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { + let state_context = self.add_assertion_context(format!( + "Initial Editor State: \"{}\"", + marked_text.escape_debug().to_string() + )); + let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); + self.editor.update(&mut self.cx, |editor, cx| { + editor.set_text(unmarked_text, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(selection_ranges) + }) + }); + state_context + } + + /// Only change the editor's selections + pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle { + let state_context = self.add_assertion_context(format!( + "Initial Editor State: \"{}\"", + marked_text.escape_debug().to_string() + )); + let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); + self.editor.update(&mut self.cx, |editor, cx| { + assert_eq!(editor.text(cx), unmarked_text); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(selection_ranges) + }) + }); + state_context + } + + /// Make an assertion about the editor's text and the ranges and directions + /// of its selections using a string containing embedded range markers. + /// + /// See the `util::test::marked_text_ranges` function for more information. + #[track_caller] + pub fn assert_editor_state(&mut self, marked_text: &str) { + let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true); + let buffer_text = self.buffer_text(); + + if buffer_text != unmarked_text { + panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}"); + } + + self.assert_selections(expected_selections, marked_text.to_string()) + } + + pub fn editor_state(&mut self) -> String { + generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true) + } + + #[track_caller] + pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { + let expected_ranges = self.ranges(marked_text); + let actual_ranges: Vec> = self.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + editor + .background_highlights + .get(&TypeId::of::()) + .map(|h| h.1.clone()) + .unwrap_or_default() + .into_iter() + .map(|range| range.to_offset(&snapshot.buffer_snapshot)) + .collect() + }); + assert_set_eq!(actual_ranges, expected_ranges); + } + + #[track_caller] + pub fn assert_editor_text_highlights(&mut self, marked_text: &str) { + let expected_ranges = self.ranges(marked_text); + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let actual_ranges: Vec> = snapshot + .text_highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| range.to_offset(&snapshot.buffer_snapshot)) + .collect(); + assert_set_eq!(actual_ranges, expected_ranges); + } + + #[track_caller] + pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { + let expected_marked_text = + generate_marked_text(&self.buffer_text(), &expected_selections, true); + self.assert_selections(expected_selections, expected_marked_text) + } + + #[track_caller] + fn editor_selections(&mut self) -> Vec> { + self.editor + .update(&mut self.cx, |editor, cx| { + editor.selections.all::(cx) + }) + .into_iter() + .map(|s| { + if s.reversed { + s.end..s.start + } else { + s.start..s.end + } + }) + .collect::>() + } + + #[track_caller] + fn assert_selections( + &mut self, + expected_selections: Vec>, + expected_marked_text: String, + ) { + let actual_selections = self.editor_selections(); + let actual_marked_text = + generate_marked_text(&self.buffer_text(), &actual_selections, true); + if expected_selections != actual_selections { + panic!( + indoc! {" + + {}Editor has unexpected selections. + + Expected selections: + {} + + Actual selections: + {} + "}, + self.assertion_context(), + expected_marked_text, + actual_marked_text, + ); + } + } +} + +impl<'a> Deref for EditorTestContext<'a> { + type Target = gpui::TestAppContext; + + fn deref(&self) -> &Self::Target { + &self.cx + } +} + +impl<'a> DerefMut for EditorTestContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx + } +} + +/// Tracks string context to be printed when assertions fail. +/// Often this is done by storing a context string in the manager and returning the handle. +#[derive(Clone)] +pub struct AssertionContextManager { + id: Arc, + contexts: Arc>>, +} + +impl AssertionContextManager { + pub fn new() -> Self { + Self { + id: Arc::new(AtomicUsize::new(0)), + contexts: Arc::new(RwLock::new(BTreeMap::new())), + } + } + + pub fn add_context(&self, context: String) -> ContextHandle { + let id = self.id.fetch_add(1, Ordering::Relaxed); + let mut contexts = self.contexts.write(); + contexts.insert(id, context); + ContextHandle { + id, + manager: self.clone(), + } + } + + pub fn context(&self) -> String { + let contexts = self.contexts.read(); + format!("\n{}\n", contexts.values().join("\n")) + } +} + +/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails. +/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails, +/// the state that was set initially for the failure can be printed in the error message +pub struct ContextHandle { + id: usize, + manager: AssertionContextManager, +} + +impl Drop for ContextHandle { + fn drop(&mut self) { + let mut contexts = self.manager.contexts.write(); + contexts.remove(&self.id); + } +} diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index abafe93cdd28947ba6e4b7cde2fa5bdd20393e53..36bd726030529f21b69f8588475cd232fe3f4069 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -5,7 +5,7 @@ use gpui::{ }; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_stack, modal, v_stack, Label, LabelColor}; +use ui::{h_stack, v_stack, Label, LabelColor, StyledExt}; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::{Modal, ModalEvent, Workspace}; @@ -148,7 +148,8 @@ impl Render for GoToLine { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - modal(cx) + div() + .elevation_2(cx) .key_context("GoToLine") .on_action(Self::cancel) .on_action(Self::confirm) diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 6526f96cb9cbc0f5be445906cc55eb2801dc429d..06e93e275d260a9d83e0d860f8f853ad00144582 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -176,8 +176,7 @@ macro_rules! actions { () => {}; ( $name:ident ) => { - #[gpui::register_action] - #[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)] + #[gpui::action] pub struct $name; }; diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 1a3d95e761f154966256a1d8b9df92c8450be578..67a713936816fa005ce794133fee388ee29b61ea 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1012,6 +1012,29 @@ impl Context for AppContext { let entity = self.entities.read(handle); read(entity, self) } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + let window = self + .windows + .get(window.id) + .ok_or_else(|| anyhow!("window not found"))? + .as_ref() + .unwrap(); + + let root_view = window.root_view.clone().unwrap(); + let view = root_view + .downcast::() + .map_err(|_| anyhow!("root view's type has changed"))?; + + Ok(read(view, self)) + } } /// These effects are processed at the end of each application update cycle. diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index e191e7315fb352a9feff55b4902432d2abc063e3..24510c18da35a07d9c676e7d2675d7dcdf876bca 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -66,6 +66,19 @@ impl Context for AsyncAppContext { let mut lock = app.borrow_mut(); lock.update_window(window, f) } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.upgrade().context("app was released")?; + let lock = app.borrow(); + lock.read_window(window, read) + } } impl AsyncAppContext { @@ -250,6 +263,17 @@ impl Context for AsyncWindowContext { { self.app.read_model(handle, read) } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + self.app.read_window(window, read) + } } impl VisualContext for AsyncWindowContext { diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index 44a3337f0321d926d21934864e18377214287b37..d04f0f22891582c8b90b124ae08756a1a95922c6 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -1,6 +1,6 @@ use crate::{ AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId, - EventEmitter, Model, Subscription, Task, WeakModel, WindowContext, + EventEmitter, Model, Subscription, Task, View, WeakModel, WindowContext, WindowHandle, }; use anyhow::Result; use derive_more::{Deref, DerefMut}; @@ -239,6 +239,17 @@ impl<'a, T> Context for ModelContext<'a, T> { { self.app.read_model(handle, read) } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + U: 'static, + { + self.app.read_window(window, read) + } } impl Borrow for ModelContext<'_, T> { diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 7b5ab5f7d76dc728556d66925dc67ca086c37893..44c31bbd69dad1325da8ec062551c3c03d67d7e2 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,12 +1,12 @@ use crate::{ - AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, - EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, - Render, Result, Task, TestDispatcher, TestPlatform, ViewContext, VisualContext, WindowContext, - WindowHandle, WindowOptions, + div, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, + Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, + ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext, + VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; -use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; +use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; #[derive(Clone)] pub struct TestAppContext { @@ -58,6 +58,18 @@ impl Context for TestAppContext { let app = self.app.borrow(); app.read_model(handle, read) } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.borrow(); + app.read_window(window, read) + } } impl TestAppContext { @@ -93,8 +105,8 @@ impl TestAppContext { Ok(()) } - pub fn executor(&self) -> &BackgroundExecutor { - &self.background_executor + pub fn executor(&self) -> BackgroundExecutor { + self.background_executor.clone() } pub fn foreground_executor(&self) -> &ForegroundExecutor { @@ -120,6 +132,26 @@ impl TestAppContext { cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)) } + pub fn add_empty_window(&mut self) -> AnyWindowHandle { + let mut cx = self.app.borrow_mut(); + cx.open_window(WindowOptions::default(), |cx| { + cx.build_view(|_| EmptyView {}) + }) + .any_handle + } + + pub fn add_window_view(&mut self, build_window: F) -> (View, VisualTestContext) + where + F: FnOnce(&mut ViewContext) -> V, + V: Render, + { + let mut cx = self.app.borrow_mut(); + let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)); + drop(cx); + let view = window.root_view(self).unwrap(); + (view, VisualTestContext::from_window(*window.deref(), self)) + } + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, @@ -146,6 +178,11 @@ impl TestAppContext { Some(read(lock.try_global()?, &lock)) } + pub fn set_global(&mut self, global: G) { + let mut lock = self.app.borrow_mut(); + lock.set_global(global); + } + pub fn update_global( &mut self, update: impl FnOnce(&mut G, &mut AppContext) -> R, @@ -259,3 +296,191 @@ impl Model { .expect("model was dropped") } } + +impl View { + pub fn condition( + &self, + cx: &TestAppContext, + mut predicate: impl FnMut(&V, &AppContext) -> bool, + ) -> impl Future + where + Evt: 'static, + V: EventEmitter, + { + use postage::prelude::{Sink as _, Stream as _}; + + let (tx, mut rx) = postage::mpsc::channel(1024); + let timeout_duration = Duration::from_millis(100); //todo!() cx.condition_duration(); + + let mut cx = cx.app.borrow_mut(); + let subscriptions = ( + cx.observe(self, { + let mut tx = tx.clone(); + move |_, _| { + tx.blocking_send(()).ok(); + } + }), + cx.subscribe(self, { + let mut tx = tx.clone(); + move |_, _: &Evt, _| { + tx.blocking_send(()).ok(); + } + }), + ); + + let cx = cx.this.upgrade().unwrap(); + let handle = self.downgrade(); + + async move { + crate::util::timeout(timeout_duration, async move { + loop { + { + let cx = cx.borrow(); + let cx = &*cx; + if predicate( + handle + .upgrade() + .expect("view dropped with pending condition") + .read(cx), + cx, + ) { + break; + } + } + + // todo!(start_waiting) + // cx.borrow().foreground_executor().start_waiting(); + rx.recv() + .await + .expect("view dropped with pending condition"); + // cx.borrow().foreground_executor().finish_waiting(); + } + }) + .await + .expect("condition timed out"); + drop(subscriptions); + } + } +} + +use derive_more::{Deref, DerefMut}; +#[derive(Deref, DerefMut)] +pub struct VisualTestContext<'a> { + #[deref] + #[deref_mut] + cx: &'a mut TestAppContext, + window: AnyWindowHandle, +} + +impl<'a> VisualTestContext<'a> { + pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { + Self { cx, window } + } +} + +impl<'a> Context for VisualTestContext<'a> { + type Result = ::Result; + + fn build_model( + &mut self, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + self.cx.build_model(build_model) + } + + fn update_model( + &mut self, + handle: &Model, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> Self::Result + where + T: 'static, + { + self.cx.update_model(handle, update) + } + + fn read_model( + &self, + handle: &Model, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static, + { + self.cx.read_model(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + self.cx.update_window(window, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + self.cx.read_window(window, read) + } +} + +impl<'a> VisualContext for VisualTestContext<'a> { + fn build_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + self.window + .update(self.cx, |_, cx| cx.build_view(build_view)) + .unwrap() + } + + fn update_view( + &mut self, + view: &View, + update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> Self::Result { + self.window + .update(self.cx, |_, cx| cx.update_view(view, update)) + .unwrap() + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: Render, + { + self.window + .update(self.cx, |_, cx| cx.replace_root_view(build_view)) + .unwrap() + } +} + +impl AnyWindowHandle { + pub fn build_view( + &self, + cx: &mut TestAppContext, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> View { + self.update(cx, |_, cx| cx.build_view(build_view)).unwrap() + } +} + +pub struct EmptyView {} + +impl Render for EmptyView { + type Element = Div; + + fn render(&mut self, _cx: &mut crate::ViewContext) -> Self::Element { + div() + } +} diff --git a/crates/gpui2/src/color.rs b/crates/gpui2/src/color.rs index 6fcb12e178991d2afb36b3f5d7e342abdd85550a..5f6308ec4fb2d2d9d7efc27eacbb42bf08109a6d 100644 --- a/crates/gpui2/src/color.rs +++ b/crates/gpui2/src/color.rs @@ -167,7 +167,7 @@ impl TryFrom<&'_ str> for Rgba { } } -#[derive(Default, Copy, Clone, Debug, PartialEq)] +#[derive(Default, Copy, Clone, Debug)] #[repr(C)] pub struct Hsla { pub h: f32, @@ -176,10 +176,63 @@ pub struct Hsla { pub a: f32, } +impl PartialEq for Hsla { + fn eq(&self, other: &Self) -> bool { + self.h + .total_cmp(&other.h) + .then(self.s.total_cmp(&other.s)) + .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) + .is_eq() + } +} + +impl PartialOrd for Hsla { + fn partial_cmp(&self, other: &Self) -> Option { + // SAFETY: The total ordering relies on this always being Some() + Some( + self.h + .total_cmp(&other.h) + .then(self.s.total_cmp(&other.s)) + .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))), + ) + } +} + +impl Ord for Hsla { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // SAFETY: The partial comparison is a total comparison + unsafe { self.partial_cmp(other).unwrap_unchecked() } + } +} + impl Hsla { pub fn to_rgb(self) -> Rgba { self.into() } + + pub fn red() -> Self { + red() + } + + pub fn green() -> Self { + green() + } + + pub fn blue() -> Self { + blue() + } + + pub fn black() -> Self { + black() + } + + pub fn white() -> Self { + white() + } + + pub fn transparent_black() -> Self { + transparent_black() + } } impl Eq for Hsla {} @@ -238,6 +291,15 @@ pub fn blue() -> Hsla { } } +pub fn green() -> Hsla { + Hsla { + h: 0.3, + s: 1., + l: 0.5, + a: 1., + } +} + impl Hsla { /// Returns true if the HSLA color is fully transparent, false otherwise. pub fn is_transparent(&self) -> bool { diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index c0f916ae966033749cd78207bd42f14597906c11..aeda28256a82775fa54a7751b0768bb951fa7014 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -101,6 +101,20 @@ pub trait InteractiveComponent: Sized + Element { self } + fn on_any_mouse_down( + mut self, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity().mouse_down_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + handler(view, event, cx) + } + }, + )); + self + } + fn on_mouse_up( mut self, button: MouseButton, @@ -119,6 +133,20 @@ pub trait InteractiveComponent: Sized + Element { self } + fn on_any_mouse_up( + mut self, + handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity().mouse_up_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + handler(view, event, cx) + } + }, + )); + self + } + fn on_mouse_down_out( mut self, handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 8688581cb8c12e1a03ef0765f7aea0396143e33d..93d087833a563b37b557fc1c09c65ce67136e8ad 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,6 +1,6 @@ use crate::{ AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString, - Size, ViewContext, + Size, TextRun, ViewContext, }; use parking_lot::Mutex; use smallvec::SmallVec; @@ -11,6 +11,7 @@ impl Component for SharedString { fn render(self) -> AnyElement { Text { text: self, + runs: None, state_type: PhantomData, } .render() @@ -21,6 +22,7 @@ impl Component for &'static str { fn render(self) -> AnyElement { Text { text: self.into(), + runs: None, state_type: PhantomData, } .render() @@ -33,6 +35,7 @@ impl Component for String { fn render(self) -> AnyElement { Text { text: self.into(), + runs: None, state_type: PhantomData, } .render() @@ -41,9 +44,25 @@ impl Component for String { pub struct Text { text: SharedString, + runs: Option>, state_type: PhantomData, } +impl Text { + /// styled renders text that has different runs of different styles. + /// callers are responsible for setting the correct style for each run. + //// + /// For uniform text you can usually just pass a string as a child, and + /// cx.text_style() will be used automatically. + pub fn styled(text: SharedString, runs: Vec) -> Self { + Text { + text, + runs: Some(runs), + state_type: Default::default(), + } + } +} + impl Component for Text { fn render(self) -> AnyElement { AnyElement::new(self) @@ -81,6 +100,13 @@ impl Element for Text { let text = self.text.clone(); let rem_size = cx.rem_size(); + + let runs = if let Some(runs) = self.runs.take() { + runs + } else { + vec![text_style.to_run(text.len())] + }; + let layout_id = cx.request_measured_layout(Default::default(), rem_size, { let element_state = element_state.clone(); move |known_dimensions, _| { @@ -88,11 +114,15 @@ impl Element for Text { .layout_text( &text, font_size, - &[text_style.to_run(text.len())], + &runs[..], known_dimensions.width, // Wrap if we know the width. ) .log_err() else { + element_state.lock().replace(TextElementState { + lines: Default::default(), + line_height, + }); return Size::default(); }; @@ -131,7 +161,8 @@ impl Element for Text { let element_state = element_state.lock(); let element_state = element_state .as_ref() - .expect("measurement has not been performed"); + .ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text)) + .unwrap(); let line_height = element_state.line_height; let mut line_origin = bounds.origin; diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 323ca5851ba09981a68e65d4ae56171326494429..51390d6be28cb84998b1bcb16e9dd81530e71c85 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -105,6 +105,14 @@ pub trait Context { fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result where F: FnOnce(AnyView, &mut WindowContext<'_>) -> T; + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static; } pub trait VisualContext: Context { diff --git a/crates/gpui2/src/input.rs b/crates/gpui2/src/input.rs index d768ce946a157cbb0e7015cc10afa922e1f1e844..140f72441794d3b66b562ba4bbc22b74e4131d4d 100644 --- a/crates/gpui2/src/input.rs +++ b/crates/gpui2/src/input.rs @@ -45,7 +45,7 @@ impl ElementInputHandler { /// containing view. pub fn new(element_bounds: Bounds, cx: &mut ViewContext) -> Self { ElementInputHandler { - view: cx.view(), + view: cx.view().clone(), element_bounds, cx: cx.to_async(), } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 5ce78553fd60b59fcb5e531453d96116afa756be..1896957ac80e799e1b72a8957ae56ac36dbabf1e 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,5 +1,5 @@ use crate::{ - div, point, Component, FocusHandle, Keystroke, Modifiers, Div, Pixels, Point, Render, + div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, ViewContext, }; use smallvec::SmallVec; @@ -286,7 +286,7 @@ pub struct FocusEvent { #[cfg(test)] mod test { use crate::{ - self as gpui, div, FocusHandle, InteractiveComponent, KeyBinding, Keystroke, Div, + self as gpui, div, Div, FocusHandle, InteractiveComponent, KeyBinding, Keystroke, ParentComponent, Render, Stateful, TestAppContext, VisualContext, }; diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index b06acae43d86f196d2d50d16d120afc26a7313b1..f737c6e30b205d867133bd86561fbcd0dc10a3e6 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -1,6 +1,6 @@ use crate::{ - build_action_from_type, Action, DispatchPhase, FocusId, KeyContext, KeyMatch, Keymap, - Keystroke, KeystrokeMatcher, WindowContext, + build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, + Keymap, Keystroke, KeystrokeMatcher, WindowContext, }; use collections::HashMap; use parking_lot::Mutex; @@ -139,6 +139,15 @@ impl DispatchTree { actions } + pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + self.keymap + .lock() + .bindings_for_action(action.type_id()) + .filter(|candidate| candidate.action.partial_eq(action)) + .cloned() + .collect() + } + pub fn dispatch_key( &mut self, keystroke: &Keystroke, diff --git a/crates/gpui2/src/keymap/binding.rs b/crates/gpui2/src/keymap/binding.rs index 9fbd0018b9541edd10ca3fe9dbf67778468f6e66..e55d664610c2ffb90f402016f8589de2f98c8f1d 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -3,9 +3,19 @@ use anyhow::Result; use smallvec::SmallVec; pub struct KeyBinding { - action: Box, - pub(super) keystrokes: SmallVec<[Keystroke; 2]>, - pub(super) context_predicate: Option, + pub(crate) action: Box, + pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, + pub(crate) context_predicate: Option, +} + +impl Clone for KeyBinding { + fn clone(&self) -> Self { + KeyBinding { + action: self.action.boxed_clone(), + keystrokes: self.keystrokes.clone(), + context_predicate: self.context_predicate.clone(), + } + } } impl KeyBinding { diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index 289ecf7e6b2b5d0231ef99d56a2364ca61695c7a..cf9143162e46a84f3a330bd2cc81747241674138 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -8,7 +8,8 @@ use parking_lot::Mutex; use crate::{ px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, - PlatformWindow, Point, Scene, Size, TileId, WindowAppearance, WindowBounds, WindowOptions, + PlatformInputHandler, PlatformWindow, Point, Scene, Size, TileId, WindowAppearance, + WindowBounds, WindowOptions, }; #[derive(Default)] @@ -23,6 +24,7 @@ pub struct TestWindow { bounds: WindowBounds, current_scene: Mutex>, display: Rc, + input_handler: Option>, handlers: Mutex, sprite_atlas: Arc, @@ -33,7 +35,7 @@ impl TestWindow { bounds: options.bounds, current_scene: Default::default(), display, - + input_handler: None, sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), } @@ -77,8 +79,8 @@ impl PlatformWindow for TestWindow { todo!() } - fn set_input_handler(&mut self, _input_handler: Box) { - todo!() + fn set_input_handler(&mut self, input_handler: Box) { + self.input_handler = Some(input_handler); } fn prompt( diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index dd0689396e2629985dbedb55bbcb3a72345bd39c..e8d6acc5a3e965818fd9198783c289297588f573 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -368,6 +368,7 @@ impl Display for FontStyle { #[derive(Clone, Debug, PartialEq, Eq)] pub struct TextRun { + // number of utf8 bytes pub len: usize, pub font: Font, pub color: Hsla, diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index 1b5e28c95836518da5ddbb16262a295ae698a2ff..db7140b04076c1fdf705ef9526ba54f691c7d81f 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/crates/gpui2/src/text_system/line_layout.rs @@ -54,9 +54,9 @@ impl LineLayout { pub fn closest_index_for_x(&self, x: Pixels) -> usize { let mut prev_index = 0; let mut prev_x = px(0.); - for run in self.runs.iter() { for glyph in run.glyphs.iter() { + glyph.index; if glyph.position.x >= x { if glyph.position.x - x < x - prev_x { return glyph.index; @@ -68,7 +68,7 @@ impl LineLayout { prev_x = glyph.position.x; } } - prev_index + prev_index + 1 } pub fn x_for_index(&self, index: usize) -> Pixels { diff --git a/crates/gpui2/src/util.rs b/crates/gpui2/src/util.rs index 1000800881e911d5e1d306d37fff88197e0c22d2..cba7ed84b58b77f6491380277ace81341e8041c5 100644 --- a/crates/gpui2/src/util.rs +++ b/crates/gpui2/src/util.rs @@ -1,16 +1,26 @@ +#[cfg(any(test, feature = "test-support"))] +use std::time::Duration; + +#[cfg(any(test, feature = "test-support"))] +use futures::Future; + +#[cfg(any(test, feature = "test-support"))] +use smol::future::FutureExt; + pub use util::*; -// pub async fn timeout(timeout: Duration, f: F) -> Result -// where -// F: Future, -// { -// let timer = async { -// smol::Timer::after(timeout).await; -// Err(()) -// }; -// let future = async move { Ok(f.await) }; -// timer.race(future).await -// } +#[cfg(any(test, feature = "test-support"))] +pub async fn timeout(timeout: Duration, f: F) -> Result +where + F: Future, +{ + let timer = async { + smol::Timer::after(timeout).await; + Err(()) + }; + let future = async move { Ok(f.await) }; + timer.race(future).await +} #[cfg(any(test, feature = "test-support"))] pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index c5d794aa3d846718229f9da540a61cb8cc51aa26..4ed7f89c7807665601f8bfe915eef56e412f6f74 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -3,15 +3,15 @@ use crate::{ AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, - InputEvent, IsZero, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, Modifiers, - MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, - PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, - PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, - SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, - TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, - WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, + Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, + PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, + RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, + Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, + WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; use derive_more::{Deref, DerefMut}; use futures::{ @@ -101,6 +101,12 @@ pub struct FocusHandle { handles: Arc>>, } +impl std::fmt::Debug for FocusHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("FocusHandle({:?})", self.id)) + } +} + impl FocusHandle { pub(crate) fn new(handles: &Arc>>) -> Self { let id = handles.write().insert(AtomicUsize::new(1)); @@ -424,6 +430,7 @@ impl<'a> WindowContext<'a> { .dispatch_tree .focusable_node_id(focus_handle.id) { + cx.propagate_event = true; cx.dispatch_action_on_node(node_id, action); } }) @@ -1377,6 +1384,13 @@ impl<'a> WindowContext<'a> { Vec::new() } } + + pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + self.window + .current_frame + .dispatch_tree + .bindings_for_action(action) + } } impl Context for WindowContext<'_> { @@ -1431,6 +1445,28 @@ impl Context for WindowContext<'_> { let entity = self.entities.read(handle); read(&*entity, &*self.app) } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + if window.any_handle == self.window.handle { + let root_view = self + .window + .root_view + .clone() + .unwrap() + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + Ok(read(root_view, self)) + } else { + self.app.read_window(window, read) + } + } } impl VisualContext for WindowContext<'_> { @@ -1746,9 +1782,12 @@ impl<'a, V: 'static> ViewContext<'a, V> { } } - // todo!("change this to return a reference"); - pub fn view(&self) -> View { - self.view.clone() + pub fn entity_id(&self) -> EntityId { + self.view.entity_id() + } + + pub fn view(&self) -> &View { + self.view } pub fn model(&self) -> Model { @@ -1771,7 +1810,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { where V: 'static, { - let view = self.view(); + let view = self.view().clone(); self.window_cx.on_next_frame(move |cx| view.update(cx, f)); } @@ -2103,7 +2142,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { &mut self, handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, ) { - let handle = self.view(); + let handle = self.view().clone(); self.window_cx.on_mouse_event(move |event, phase, cx| { handle.update(cx, |view, cx| { handler(view, event, phase, cx); @@ -2115,7 +2154,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { &mut self, handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, ) { - let handle = self.view(); + let handle = self.view().clone(); self.window_cx.on_key_event(move |event, phase, cx| { handle.update(cx, |view, cx| { handler(view, event, phase, cx); @@ -2128,7 +2167,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { action_type: TypeId, handler: impl Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext) + 'static, ) { - let handle = self.view(); + let handle = self.view().clone(); self.window_cx .on_action(action_type, move |action, phase, cx| { handle.update(cx, |view, cx| { @@ -2203,6 +2242,17 @@ impl Context for ViewContext<'_, V> { { self.window_cx.read_model(handle, read) } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + self.window_cx.read_window(window, read) + } } impl VisualContext for ViewContext<'_, V> { @@ -2275,7 +2325,7 @@ impl WindowHandle { } pub fn update( - self, + &self, cx: &mut C, update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, ) -> Result @@ -2289,6 +2339,36 @@ impl WindowHandle { Ok(cx.update_view(&view, update)) })? } + + pub fn read<'a>(&self, cx: &'a AppContext) -> Result<&'a V> { + let x = cx + .windows + .get(self.id) + .and_then(|window| { + window + .as_ref() + .and_then(|window| window.root_view.clone()) + .map(|root_view| root_view.downcast::()) + }) + .ok_or_else(|| anyhow!("window not found"))? + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + + Ok(x.read(cx)) + } + + pub fn read_with(&self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result + where + C: Context, + { + cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx)) + } + + pub fn root_view(&self, cx: &C) -> Result> + where + C: Context, + { + cx.read_window(self, |root_view, _cx| root_view.clone()) + } } impl Copy for WindowHandle {} @@ -2354,6 +2434,18 @@ impl AnyWindowHandle { { cx.update_window(self, update) } + + pub fn read(self, cx: &C, read: impl FnOnce(View, &AppContext) -> R) -> Result + where + C: Context, + T: 'static, + { + let view = self + .downcast::() + .context("the type of the window's root view has changed")?; + + cx.read_window(&view, read) + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/gpui2_macros/src/action.rs b/crates/gpui2_macros/src/action.rs index 66302f3fc0b6ee519e349376ca65fda8b2e4a783..564f35d6a4da2d873afb5e4a059c295b11d1ef58 100644 --- a/crates/gpui2_macros/src/action.rs +++ b/crates/gpui2_macros/src/action.rs @@ -34,13 +34,21 @@ pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream { let visibility = input.vis; let output = match input.data { - syn::Data::Struct(ref struct_data) => { - let fields = &struct_data.fields; - quote! { - #attributes - #visibility struct #name #fields + syn::Data::Struct(ref struct_data) => match &struct_data.fields { + syn::Fields::Named(_) | syn::Fields::Unnamed(_) => { + let fields = &struct_data.fields; + quote! { + #attributes + #visibility struct #name #fields + } } - } + syn::Fields::Unit => { + quote! { + #attributes + #visibility struct #name; + } + } + }, syn::Data::Enum(ref enum_data) => { let variants = &enum_data.variants; quote! { diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index bdea440c08d3c209dfb363ec33c1ae725ffb742e..311049f0328dac56df8e04720b7f2c74136234af 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -1858,7 +1858,7 @@ mod tests { async fn test_first_line_pattern(cx: &mut TestAppContext) { let mut languages = LanguageRegistry::test(); - languages.set_executor(cx.executor().clone()); + languages.set_executor(cx.executor()); let languages = Arc::new(languages); languages.register( "/javascript", @@ -1895,7 +1895,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_language_loading(cx: &mut TestAppContext) { let mut languages = LanguageRegistry::test(); - languages.set_executor(cx.executor().clone()); + languages.set_executor(cx.executor()); let languages = Arc::new(languages); languages.register( "/JSON", diff --git a/crates/menu2/src/menu2.rs b/crates/menu2/src/menu2.rs index 44ebffcca2de58e43f5452d6975cb54462ed33bb..6dfcce5d4f751c3e990d1a3c440ddebdf237d162 100644 --- a/crates/menu2/src/menu2.rs +++ b/crates/menu2/src/menu2.rs @@ -1,9 +1,13 @@ use gpui::actions; -// todo!(remove this) +// If the zed binary doesn't use anything in this crate, it will be optimized away +// and the actions won't initialize. So we just provide an empty initialization function +// to be called from main. +// +// These may provide relevant context: // https://github.com/rust-lang/rust/issues/47384 // https://github.com/mmastrac/rust-ctor/issues/280 -pub fn unused() {} +pub fn init() {} actions!( Cancel, diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 0a2a50deb6a1610011c893b843d5daa01ad748cb..199941ef3b80ccc650396a985424f0b300f5b06d 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ - div, uniform_list, Component, Div, ParentComponent, Render, Styled, Task, - UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, + div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task, + UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; @@ -10,7 +10,8 @@ pub struct Picker { pub delegate: D, scroll_handle: UniformListScrollHandle, editor: View, - pending_update_matches: Option>>, + pending_update_matches: Option>, + confirm_on_update: Option, } pub trait PickerDelegate: Sized + 'static { @@ -42,12 +43,15 @@ impl Picker { editor }); cx.subscribe(&editor, Self::on_input_editor_event).detach(); - Self { + let mut this = Self { delegate, + editor, scroll_handle: UniformListScrollHandle::new(), pending_update_matches: None, - editor, - } + confirm_on_update: None, + }; + this.update_matches("".to_string(), cx); + this } pub fn focus(&self, cx: &mut WindowContext) { @@ -99,11 +103,26 @@ impl Picker { } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - self.delegate.confirm(false, cx); + if self.pending_update_matches.is_some() { + self.confirm_on_update = Some(false) + } else { + self.delegate.confirm(false, cx); + } } fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { - self.delegate.confirm(true, cx); + if self.pending_update_matches.is_some() { + self.confirm_on_update = Some(true) + } else { + self.delegate.confirm(true, cx); + } + } + + fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext) { + cx.stop_propagation(); + cx.prevent_default(); + self.delegate.set_selected_index(ix, cx); + self.delegate.confirm(secondary, cx); } fn on_input_editor_event( @@ -126,7 +145,7 @@ impl Picker { this.update(&mut cx, |this, cx| { this.matches_updated(cx); }) - .ok() + .ok(); })); } @@ -134,6 +153,9 @@ impl Picker { let index = self.delegate.selected_index(); self.scroll_handle.scroll_to_item(index); self.pending_update_matches = None; + if let Some(secondary) = self.confirm_on_update.take() { + self.delegate.confirm(secondary, cx); + } cx.notify(); } } @@ -171,7 +193,22 @@ impl Render for Picker { let selected_ix = this.delegate.selected_index(); visible_range .map(|ix| { - this.delegate.render_match(ix, ix == selected_ix, cx) + div() + .on_mouse_down( + MouseButton::Left, + move |this: &mut Self, event, cx| { + this.handle_click( + ix, + event.modifiers.command, + cx, + ) + }, + ) + .child(this.delegate.render_match( + ix, + ix == selected_ix, + cx, + )) }) .collect() } @@ -184,10 +221,11 @@ impl Render for Picker { }) .when(self.delegate.match_count() == 0, |el| { el.child( - v_stack() - .p_1() - .grow() - .child(Label::new("No matches").color(LabelColor::Muted)), + v_stack().p_1().grow().child( + div() + .px_1() + .child(Label::new("No matches").color(LabelColor::Muted)), + ), ) }) } diff --git a/crates/prettier2/src/prettier2.rs b/crates/prettier2/src/prettier2.rs index faa97bad7e981960eea5c614c22eb226c7c27d08..a01144ced330fa3bfcb75ac8330c6f526b25dfb1 100644 --- a/crates/prettier2/src/prettier2.rs +++ b/crates/prettier2/src/prettier2.rs @@ -497,7 +497,7 @@ mod tests { #[gpui::test] async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -573,7 +573,7 @@ mod tests { #[gpui::test] async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -638,7 +638,7 @@ mod tests { #[gpui::test] async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -731,7 +731,7 @@ mod tests { #[gpui::test] async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -812,7 +812,7 @@ mod tests { async fn test_prettier_lookup_in_npm_workspaces_for_not_installed( cx: &mut gpui::TestAppContext, ) { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 61b0f486d31620b14ad8d12d6ba675250277ad7f..efe407f847baabda23811994e324ace3630a7756 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -863,7 +863,7 @@ impl Project { cx: &mut gpui::TestAppContext, ) -> Model { let mut languages = LanguageRegistry::test(); - languages.set_executor(cx.executor().clone()); + languages.set_executor(cx.executor()); let http_client = util::http::FakeHttpClient::with_404_response(); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx)); diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index 19485b2306ad3d34377cf188338b0ac030baaa06..97b6ed9c7429be0640aff708447a017ed86f30ef 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -89,7 +89,7 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) { async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/the-root", json!({ @@ -189,7 +189,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/the-root", json!({ @@ -547,7 +547,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/the-root", json!({ @@ -734,7 +734,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -826,7 +826,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -914,7 +914,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -1046,7 +1046,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "" })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; @@ -1125,7 +1125,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "x" })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; @@ -1215,7 +1215,7 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "" })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; @@ -1279,7 +1279,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) .await; @@ -1401,7 +1401,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { " .unindent(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": text })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; @@ -1671,7 +1671,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { "let three = 3;\n", ); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": text })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; @@ -1734,7 +1734,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "one two three" })) .await; @@ -1813,7 +1813,7 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { " .unindent(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -1959,7 +1959,7 @@ async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAp " .unindent(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2067,7 +2067,7 @@ async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) { " .unindent(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2187,7 +2187,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { ); let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2299,7 +2299,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2396,7 +2396,7 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2451,7 +2451,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2559,7 +2559,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { async fn test_save_file(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2591,7 +2591,7 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2622,7 +2622,7 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { async fn test_save_as(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({})).await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; @@ -2830,7 +2830,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2881,7 +2881,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2927,7 +2927,7 @@ async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3074,7 +3074,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { init_test(cx); let initial_contents = "aaa\nbbbbb\nc\n"; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3154,7 +3154,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3216,7 +3216,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/the-dir", json!({ @@ -3479,7 +3479,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3596,7 +3596,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { async fn test_search(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3655,7 +3655,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { let search_query = "file"; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3767,7 +3767,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { let search_query = "file"; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3878,7 +3878,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex let search_query = "file"; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..e3e04f5254f84009b86d02f59f9ce9894a3ab8b7 --- /dev/null +++ b/crates/project_panel2/src/project_panel.rs @@ -0,0 +1,2868 @@ +pub mod file_associations; +mod project_panel_settings; +use settings::Settings; + +use db::kvp::KEY_VALUE_STORE; +use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; +use file_associations::FileAssociations; + +use anyhow::{anyhow, Result}; +use gpui::{ + actions, div, px, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, + AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusEnabled, + FocusHandle, Model, ParentElement as _, Pixels, Point, PromptLevel, Render, + StatefulInteractive, StatefulInteractivity, Styled, Task, UniformListScrollHandle, View, + ViewContext, VisualContext as _, WeakView, WindowContext, +}; +use menu::{Confirm, SelectNext, SelectPrev}; +use project::{ + repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, + Worktree, WorktreeId, +}; +use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use std::{ + cmp::Ordering, + collections::{hash_map, HashMap}, + ffi::OsStr, + ops::Range, + path::Path, + sync::Arc, +}; +use theme::ActiveTheme as _; +use ui::{h_stack, v_stack}; +use unicase::UniCase; +use util::TryFutureExt; +use workspace::{ + dock::{DockPosition, PanelEvent}, + Workspace, +}; + +const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; +const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; + +pub struct ProjectPanel { + project: Model, + fs: Arc, + list: UniformListScrollHandle, + focus_handle: FocusHandle, + visible_entries: Vec<(WorktreeId, Vec)>, + last_worktree_root_id: Option, + expanded_dir_ids: HashMap>, + selection: Option, + edit_state: Option, + filename_editor: View, + clipboard_entry: Option, + dragged_entry_destination: Option>, + workspace: WeakView, + has_focus: bool, + width: Option, + pending_serialization: Task>, +} + +#[derive(Copy, Clone, Debug)] +struct Selection { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, +} + +#[derive(Clone, Debug)] +struct EditState { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + is_new_entry: bool, + is_dir: bool, + processing_filename: Option, +} + +#[derive(Copy, Clone)] +pub enum ClipboardEntry { + Copied { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + }, + Cut { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + }, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct EntryDetails { + filename: String, + icon: Option>, + path: Arc, + depth: usize, + kind: EntryKind, + is_ignored: bool, + is_expanded: bool, + is_selected: bool, + is_editing: bool, + is_processing: bool, + is_cut: bool, + git_status: Option, +} + +actions!( + ExpandSelectedEntry, + CollapseSelectedEntry, + CollapseAllEntries, + NewDirectory, + NewFile, + Copy, + CopyPath, + CopyRelativePath, + RevealInFinder, + OpenInTerminal, + Cut, + Paste, + Delete, + Rename, + Open, + ToggleFocus, + NewSearchInDirectory, +); + +pub fn init_settings(cx: &mut AppContext) { + ProjectPanelSettings::register(cx); +} + +pub fn init(assets: impl AssetSource, cx: &mut AppContext) { + init_settings(cx); + file_associations::init(assets, cx); + + // cx.add_action(ProjectPanel::expand_selected_entry); + // cx.add_action(ProjectPanel::collapse_selected_entry); + // cx.add_action(ProjectPanel::collapse_all_entries); + // cx.add_action(ProjectPanel::select_prev); + // cx.add_action(ProjectPanel::select_next); + // cx.add_action(ProjectPanel::new_file); + // cx.add_action(ProjectPanel::new_directory); + // cx.add_action(ProjectPanel::rename); + // cx.add_async_action(ProjectPanel::delete); + // cx.add_async_action(ProjectPanel::confirm); + // cx.add_async_action(ProjectPanel::open_file); + // cx.add_action(ProjectPanel::cancel); + // cx.add_action(ProjectPanel::cut); + // cx.add_action(ProjectPanel::copy); + // cx.add_action(ProjectPanel::copy_path); + // cx.add_action(ProjectPanel::copy_relative_path); + // cx.add_action(ProjectPanel::reveal_in_finder); + // cx.add_action(ProjectPanel::open_in_terminal); + // cx.add_action(ProjectPanel::new_search_in_directory); + // cx.add_action( + // |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { + // this.paste(action, cx); + // }, + // ); +} + +#[derive(Debug)] +pub enum Event { + OpenedEntry { + entry_id: ProjectEntryId, + focus_opened_item: bool, + }, + SplitEntry { + entry_id: ProjectEntryId, + }, + DockPositionChanged, + Focus, + NewSearchInDirectory { + dir_entry: Entry, + }, + ActivatePanel, +} + +#[derive(Serialize, Deserialize)] +struct SerializedProjectPanel { + width: Option, +} + +impl ProjectPanel { + fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { + let project = workspace.project().clone(); + let project_panel = cx.build_view(|cx: &mut ViewContext| { + cx.observe(&project, |this, _, cx| { + this.update_visible_entries(None, cx); + cx.notify(); + }) + .detach(); + let focus_handle = cx.focus_handle(); + + cx.on_focus(&focus_handle, Self::focus_in).detach(); + cx.on_blur(&focus_handle, Self::focus_out).detach(); + + cx.subscribe(&project, |this, project, event, cx| match event { + project::Event::ActiveEntryChanged(Some(entry_id)) => { + if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx) + { + this.expand_entry(worktree_id, *entry_id, cx); + this.update_visible_entries(Some((worktree_id, *entry_id)), cx); + this.autoscroll(cx); + cx.notify(); + } + } + project::Event::ActivateProjectPanel => { + cx.emit(Event::ActivatePanel); + } + project::Event::WorktreeRemoved(id) => { + this.expanded_dir_ids.remove(id); + this.update_visible_entries(None, cx); + cx.notify(); + } + _ => {} + }) + .detach(); + + let filename_editor = cx.build_view(|cx| Editor::single_line(cx)); + + cx.subscribe(&filename_editor, |this, _, event, cx| match event { + editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => { + this.autoscroll(cx); + } + _ => {} + }) + .detach(); + + // cx.observe_focus(&filename_editor, |this, _, is_focused, cx| { + // if !is_focused + // && this + // .edit_state + // .as_ref() + // .map_or(false, |state| state.processing_filename.is_none()) + // { + // this.edit_state = None; + // this.update_visible_entries(None, cx); + // } + // }) + // .detach(); + + // cx.observe_global::(|_, cx| { + // cx.notify(); + // }) + // .detach(); + + let view_id = cx.view().entity_id(); + let mut this = Self { + project: project.clone(), + fs: workspace.app_state().fs.clone(), + list: UniformListScrollHandle::new(), + focus_handle, + visible_entries: Default::default(), + last_worktree_root_id: Default::default(), + expanded_dir_ids: Default::default(), + selection: None, + edit_state: None, + filename_editor, + clipboard_entry: None, + // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), + dragged_entry_destination: None, + workspace: workspace.weak_handle(), + has_focus: false, + width: None, + pending_serialization: Task::ready(None), + }; + this.update_visible_entries(None, cx); + + // Update the dock position when the setting changes. + // todo!() + // let mut old_dock_position = this.position(cx); + // cx.observe_global::(move |this, cx| { + // let new_dock_position = this.position(cx); + // if new_dock_position != old_dock_position { + // old_dock_position = new_dock_position; + // cx.emit(Event::DockPositionChanged); + // } + // }) + // .detach(); + + this + }); + + cx.subscribe(&project_panel, { + let project_panel = project_panel.downgrade(); + move |workspace, _, event, cx| match event { + &Event::OpenedEntry { + entry_id, + focus_opened_item, + } => { + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { + if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + workspace + .open_path( + ProjectPath { + worktree_id: worktree.read(cx).id(), + path: entry.path.clone(), + }, + None, + focus_opened_item, + cx, + ) + .detach_and_log_err(cx); + if !focus_opened_item { + if let Some(project_panel) = project_panel.upgrade() { + let focus_handle = project_panel.read(cx).focus_handle.clone(); + cx.focus(&focus_handle); + } + } + } + } + } + &Event::SplitEntry { entry_id } => { + // if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { + // if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + // workspace + // .split_path( + // ProjectPath { + // worktree_id: worktree.read(cx).id(), + // path: entry.path.clone(), + // }, + // cx, + // ) + // .detach_and_log_err(cx); + // } + // } + } + _ => {} + } + }) + .detach(); + + project_panel + } + + pub fn load( + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + // let serialized_panel = if let Some(panel) = cx + // .background_executor() + // .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) }) + // .await + // .log_err() + // .flatten() + // { + // Some(serde_json::from_str::(&panel)?) + // } else { + // None + // }; + workspace.update(&mut cx, |workspace, cx| { + let panel = ProjectPanel::new(workspace, cx); + // if let Some(serialized_panel) = serialized_panel { + // panel.update(cx, |panel, cx| { + // panel.width = serialized_panel.width; + // cx.notify(); + // }); + // } + panel + }) + }) + } + + fn serialize(&mut self, cx: &mut ViewContext) { + let width = self.width; + self.pending_serialization = cx.background_executor().spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + PROJECT_PANEL_KEY.into(), + serde_json::to_string(&SerializedProjectPanel { width })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ); + } + + fn focus_in(&mut self, cx: &mut ViewContext) { + if !self.has_focus { + self.has_focus = true; + cx.emit(Event::Focus); + } + } + + fn focus_out(&mut self, _: &mut ViewContext) { + self.has_focus = false; + } + + fn deploy_context_menu( + &mut self, + position: Point, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) { + // let project = self.project.read(cx); + + // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { + // id + // } else { + // return; + // }; + + // self.selection = Some(Selection { + // worktree_id, + // entry_id, + // }); + + // let mut menu_entries = Vec::new(); + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let is_root = Some(entry) == worktree.root_entry(); + // if !project.is_remote() { + // menu_entries.push(ContextMenuItem::action( + // "Add Folder to Project", + // workspace::AddFolderToProject, + // )); + // if is_root { + // let project = self.project.clone(); + // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { + // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); + // })); + // } + // } + // menu_entries.push(ContextMenuItem::action("New File", NewFile)); + // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Cut", Cut)); + // menu_entries.push(ContextMenuItem::action("Copy", Copy)); + // if let Some(clipboard_entry) = self.clipboard_entry { + // if clipboard_entry.worktree_id() == worktree.id() { + // menu_entries.push(ContextMenuItem::action("Paste", Paste)); + // } + // } + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); + // menu_entries.push(ContextMenuItem::action( + // "Copy Relative Path", + // CopyRelativePath, + // )); + + // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::Separator); + // } + // menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); + // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); + // menu_entries.push(ContextMenuItem::action( + // "Search Inside", + // NewSearchInDirectory, + // )); + // } + + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Rename", Rename)); + // if !is_root { + // menu_entries.push(ContextMenuItem::action("Delete", Delete)); + // } + // } + + // // self.context_menu.update(cx, |menu, cx| { + // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); + // // }); + + // cx.notify(); + } + + fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + if entry.is_dir() { + let worktree_id = worktree.id(); + let entry_id = entry.id; + let expanded_dir_ids = + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + expanded_dir_ids + } else { + return; + }; + + match expanded_dir_ids.binary_search(&entry_id) { + Ok(_) => self.select_next(&SelectNext, cx), + Err(ix) => { + self.project.update(cx, |project, cx| { + project.expand_entry(worktree_id, entry_id, cx); + }); + + expanded_dir_ids.insert(ix, entry_id); + self.update_visible_entries(None, cx); + cx.notify(); + } + } + } + } + } + + fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext) { + if let Some((worktree, mut entry)) = self.selected_entry(cx) { + let worktree_id = worktree.id(); + let expanded_dir_ids = + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + expanded_dir_ids + } else { + return; + }; + + loop { + let entry_id = entry.id; + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.notify(); + break; + } + Err(_) => { + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } + } + } + } + } + } + + pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext) { + self.expanded_dir_ids.clear(); + self.update_visible_entries(None, cx); + cx.notify(); + } + + fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + self.project.update(cx, |project, cx| { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + } + Err(ix) => { + project.expand_entry(worktree_id, entry_id, cx); + expanded_dir_ids.insert(ix, entry_id); + } + } + }); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.focus(&self.focus_handle); + cx.notify(); + } + } + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + let (mut worktree_ix, mut entry_ix, _) = + self.index_for_selection(selection).unwrap_or_default(); + if entry_ix > 0 { + entry_ix -= 1; + } else if worktree_ix > 0 { + worktree_ix -= 1; + entry_ix = self.visible_entries[worktree_ix].1.len() - 1; + } else { + return; + } + + let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix]; + self.selection = Some(Selection { + worktree_id: *worktree_id, + entry_id: worktree_entries[entry_ix].id, + }); + self.autoscroll(cx); + cx.notify(); + } else { + self.select_first(cx); + } + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { + if let Some(task) = self.confirm_edit(cx) { + return Some(task); + } + + None + } + + fn open_file(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { + if let Some((_, entry)) = self.selected_entry(cx) { + if entry.is_file() { + self.open_entry(entry.id, true, cx); + } + } + + None + } + + fn confirm_edit(&mut self, cx: &mut ViewContext) -> Option>> { + let edit_state = self.edit_state.as_mut()?; + cx.focus(&self.focus_handle); + + let worktree_id = edit_state.worktree_id; + let is_new_entry = edit_state.is_new_entry; + let is_dir = edit_state.is_dir; + let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; + let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone(); + let filename = self.filename_editor.read(cx).text(cx); + + let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some(); + let edit_task; + let edited_entry_id; + if is_new_entry { + self.selection = Some(Selection { + worktree_id, + entry_id: NEW_ENTRY_ID, + }); + let new_path = entry.path.join(&filename.trim_start_matches("/")); + if path_already_exists(new_path.as_path()) { + return None; + } + + edited_entry_id = NEW_ENTRY_ID; + edit_task = self.project.update(cx, |project, cx| { + project.create_entry((worktree_id, &new_path), is_dir, cx) + })?; + } else { + let new_path = if let Some(parent) = entry.path.clone().parent() { + parent.join(&filename) + } else { + filename.clone().into() + }; + if path_already_exists(new_path.as_path()) { + return None; + } + + edited_entry_id = entry.id; + edit_task = self.project.update(cx, |project, cx| { + project.rename_entry(entry.id, new_path.as_path(), cx) + })?; + }; + + edit_state.processing_filename = Some(filename); + cx.notify(); + + Some(cx.spawn(|this, mut cx| async move { + let new_entry = edit_task.await; + this.update(&mut cx, |this, cx| { + this.edit_state.take(); + cx.notify(); + })?; + + let new_entry = new_entry?; + this.update(&mut cx, |this, cx| { + if let Some(selection) = &mut this.selection { + if selection.entry_id == edited_entry_id { + selection.worktree_id = worktree_id; + selection.entry_id = new_entry.id; + this.expand_to_selection(cx); + } + } + this.update_visible_entries(None, cx); + if is_new_entry && !is_dir { + this.open_entry(new_entry.id, true, cx); + } + cx.notify(); + })?; + Ok(()) + })) + } + + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + self.edit_state = None; + self.update_visible_entries(None, cx); + cx.focus(&self.focus_handle); + cx.notify(); + } + + fn open_entry( + &mut self, + entry_id: ProjectEntryId, + focus_opened_item: bool, + cx: &mut ViewContext, + ) { + cx.emit(Event::OpenedEntry { + entry_id, + focus_opened_item, + }); + } + + fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { + cx.emit(Event::SplitEntry { entry_id }); + } + + fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext) { + self.add_entry(false, cx) + } + + fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext) { + self.add_entry(true, cx) + } + + fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext) { + if let Some(Selection { + worktree_id, + entry_id, + }) = self.selection + { + let directory_id; + if let Some((worktree, expanded_dir_ids)) = self + .project + .read(cx) + .worktree_for_id(worktree_id, cx) + .zip(self.expanded_dir_ids.get_mut(&worktree_id)) + { + let worktree = worktree.read(cx); + if let Some(mut entry) = worktree.entry_for_id(entry_id) { + loop { + if entry.is_dir() { + if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(ix, entry.id); + } + directory_id = entry.id; + break; + } else { + if let Some(parent_path) = entry.path.parent() { + if let Some(parent_entry) = worktree.entry_for_path(parent_path) { + entry = parent_entry; + continue; + } + } + return; + } + } + } else { + return; + }; + } else { + return; + }; + + self.edit_state = Some(EditState { + worktree_id, + entry_id: directory_id, + is_new_entry: true, + is_dir, + processing_filename: None, + }); + self.filename_editor.update(cx, |editor, cx| { + editor.clear(cx); + editor.focus(cx); + }); + self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx); + self.autoscroll(cx); + cx.notify(); + } + } + + fn rename(&mut self, _: &Rename, cx: &mut ViewContext) { + if let Some(Selection { + worktree_id, + entry_id, + }) = self.selection + { + if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { + if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + self.edit_state = Some(EditState { + worktree_id, + entry_id, + is_new_entry: false, + is_dir: entry.is_dir(), + processing_filename: None, + }); + let file_name = entry + .path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_default() + .to_string(); + let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy()); + let selection_end = + file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); + self.filename_editor.update(cx, |editor, cx| { + editor.set_text(file_name, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([0..selection_end]) + }); + editor.focus(cx); + }); + self.update_visible_entries(None, cx); + self.autoscroll(cx); + cx.notify(); + } + } + + // cx.update_global(|drag_and_drop: &mut DragAndDrop, cx| { + // drag_and_drop.cancel_dragging::(cx); + // }) + } + } + + fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { + let Selection { entry_id, .. } = self.selection?; + let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; + let file_name = path.file_name()?; + + let mut answer = cx.prompt( + PromptLevel::Info, + &format!("Delete {file_name:?}?"), + &["Delete", "Cancel"], + ); + Some(cx.spawn(|this, mut cx| async move { + if answer.await != Ok(0) { + return Ok(()); + } + this.update(&mut cx, |this, cx| { + this.project + .update(cx, |project, cx| project.delete_entry(entry_id, cx)) + .ok_or_else(|| anyhow!("no such entry")) + })?? + .await + })) + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + let (mut worktree_ix, mut entry_ix, _) = + self.index_for_selection(selection).unwrap_or_default(); + if let Some((_, worktree_entries)) = self.visible_entries.get(worktree_ix) { + if entry_ix + 1 < worktree_entries.len() { + entry_ix += 1; + } else { + worktree_ix += 1; + entry_ix = 0; + } + } + + if let Some((worktree_id, worktree_entries)) = self.visible_entries.get(worktree_ix) { + if let Some(entry) = worktree_entries.get(entry_ix) { + self.selection = Some(Selection { + worktree_id: *worktree_id, + entry_id: entry.id, + }); + self.autoscroll(cx); + cx.notify(); + } + } + } else { + self.select_first(cx); + } + } + + fn select_first(&mut self, cx: &mut ViewContext) { + let worktree = self + .visible_entries + .first() + .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx)); + if let Some(worktree) = worktree { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + if let Some(root_entry) = worktree.root_entry() { + self.selection = Some(Selection { + worktree_id, + entry_id: root_entry.id, + }); + self.autoscroll(cx); + cx.notify(); + } + } + } + + fn autoscroll(&mut self, cx: &mut ViewContext) { + if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { + self.list.scroll_to_item(index); + cx.notify(); + } + } + + fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + self.clipboard_entry = Some(ClipboardEntry::Cut { + worktree_id: worktree.id(), + entry_id: entry.id, + }); + cx.notify(); + } + } + + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + self.clipboard_entry = Some(ClipboardEntry::Copied { + worktree_id: worktree.id(), + entry_id: entry.id, + }); + cx.notify(); + } + } + + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) -> Option<()> { + if let Some((worktree, entry)) = self.selected_entry(cx) { + let clipboard_entry = self.clipboard_entry?; + if clipboard_entry.worktree_id() != worktree.id() { + return None; + } + + let clipboard_entry_file_name = self + .project + .read(cx) + .path_for_entry(clipboard_entry.entry_id(), cx)? + .path + .file_name()? + .to_os_string(); + + let mut new_path = entry.path.to_path_buf(); + if entry.is_file() { + new_path.pop(); + } + + new_path.push(&clipboard_entry_file_name); + let extension = new_path.extension().map(|e| e.to_os_string()); + let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?; + let mut ix = 0; + while worktree.entry_for_path(&new_path).is_some() { + new_path.pop(); + + let mut new_file_name = file_name_without_extension.to_os_string(); + new_file_name.push(" copy"); + if ix > 0 { + new_file_name.push(format!(" {}", ix)); + } + if let Some(extension) = extension.as_ref() { + new_file_name.push("."); + new_file_name.push(extension); + } + + new_path.push(new_file_name); + ix += 1; + } + + if clipboard_entry.is_cut() { + if let Some(task) = self.project.update(cx, |project, cx| { + project.rename_entry(clipboard_entry.entry_id(), new_path, cx) + }) { + task.detach_and_log_err(cx) + } + } else if let Some(task) = self.project.update(cx, |project, cx| { + project.copy_entry(clipboard_entry.entry_id(), new_path, cx) + }) { + task.detach_and_log_err(cx) + } + } + None + } + + fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + cx.write_to_clipboard(ClipboardItem::new( + worktree + .abs_path() + .join(&entry.path) + .to_string_lossy() + .to_string(), + )); + } + } + + fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext) { + if let Some((_, entry)) = self.selected_entry(cx) { + cx.write_to_clipboard(ClipboardItem::new(entry.path.to_string_lossy().to_string())); + } + } + + fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + cx.reveal_path(&worktree.abs_path().join(&entry.path)); + } + } + + fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { + todo!() + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let window = cx.window(); + // let view_id = cx.view_id(); + // let path = worktree.abs_path().join(&entry.path); + + // cx.app_context() + // .spawn(|mut cx| async move { + // window.dispatch_action( + // view_id, + // &workspace::OpenTerminal { + // working_directory: path, + // }, + // &mut cx, + // ); + // }) + // .detach(); + // } + } + + pub fn new_search_in_directory( + &mut self, + _: &NewSearchInDirectory, + cx: &mut ViewContext, + ) { + if let Some((_, entry)) = self.selected_entry(cx) { + if entry.is_dir() { + cx.emit(Event::NewSearchInDirectory { + dir_entry: entry.clone(), + }); + } + } + } + + fn move_entry( + &mut self, + entry_to_move: ProjectEntryId, + destination: ProjectEntryId, + destination_is_file: bool, + cx: &mut ViewContext, + ) { + let destination_worktree = self.project.update(cx, |project, cx| { + let entry_path = project.path_for_entry(entry_to_move, cx)?; + let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + + let mut destination_path = destination_entry_path.as_ref(); + if destination_is_file { + destination_path = destination_path.parent()?; + } + + let mut new_path = destination_path.to_path_buf(); + new_path.push(entry_path.path.file_name()?); + if new_path != entry_path.path.as_ref() { + let task = project.rename_entry(entry_to_move, new_path, cx)?; + cx.foreground_executor().spawn(task).detach_and_log_err(cx); + } + + Some(project.worktree_id_for_entry(destination, cx)?) + }); + + if let Some(destination_worktree) = destination_worktree { + self.expand_entry(destination_worktree, destination, cx); + } + } + + fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { + let mut entry_index = 0; + let mut visible_entries_index = 0; + for (worktree_index, (worktree_id, worktree_entries)) in + self.visible_entries.iter().enumerate() + { + if *worktree_id == selection.worktree_id { + for entry in worktree_entries { + if entry.id == selection.entry_id { + return Some((worktree_index, entry_index, visible_entries_index)); + } else { + visible_entries_index += 1; + entry_index += 1; + } + } + break; + } else { + visible_entries_index += worktree_entries.len(); + } + } + None + } + + pub fn selected_entry<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(&'a Worktree, &'a project::Entry)> { + let (worktree, entry) = self.selected_entry_handle(cx)?; + Some((worktree.read(cx), entry)) + } + + fn selected_entry_handle<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(Model, &'a project::Entry)> { + let selection = self.selection?; + let project = self.project.read(cx); + let worktree = project.worktree_for_id(selection.worktree_id, cx)?; + let entry = worktree.read(cx).entry_for_id(selection.entry_id)?; + Some((worktree, entry)) + } + + fn expand_to_selection(&mut self, cx: &mut ViewContext) -> Option<()> { + let (worktree, entry) = self.selected_entry(cx)?; + let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default(); + + for path in entry.path.ancestors() { + let Some(entry) = worktree.entry_for_path(path) else { + continue; + }; + if entry.is_dir() { + if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(idx, entry.id); + } + } + } + + Some(()) + } + + fn update_visible_entries( + &mut self, + new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, + cx: &mut ViewContext, + ) { + let project = self.project.read(cx); + self.last_worktree_root_id = project + .visible_worktrees(cx) + .rev() + .next() + .and_then(|worktree| worktree.read(cx).root_entry()) + .map(|entry| entry.id); + + self.visible_entries.clear(); + for worktree in project.visible_worktrees(cx) { + let snapshot = worktree.read(cx).snapshot(); + let worktree_id = snapshot.id(); + + let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) { + hash_map::Entry::Occupied(e) => e.into_mut(), + hash_map::Entry::Vacant(e) => { + // The first time a worktree's root entry becomes available, + // mark that root entry as expanded. + if let Some(entry) = snapshot.root_entry() { + e.insert(vec![entry.id]).as_slice() + } else { + &[] + } + } + }; + + let mut new_entry_parent_id = None; + let mut new_entry_kind = EntryKind::Dir; + if let Some(edit_state) = &self.edit_state { + if edit_state.worktree_id == worktree_id && edit_state.is_new_entry { + new_entry_parent_id = Some(edit_state.entry_id); + new_entry_kind = if edit_state.is_dir { + EntryKind::Dir + } else { + EntryKind::File(Default::default()) + }; + } + } + + let mut visible_worktree_entries = Vec::new(); + let mut entry_iter = snapshot.entries(true); + + while let Some(entry) = entry_iter.entry() { + visible_worktree_entries.push(entry.clone()); + if Some(entry.id) == new_entry_parent_id { + visible_worktree_entries.push(Entry { + id: NEW_ENTRY_ID, + kind: new_entry_kind, + path: entry.path.join("\0").into(), + inode: 0, + mtime: entry.mtime, + is_symlink: false, + is_ignored: false, + is_external: false, + git_status: entry.git_status, + }); + } + if expanded_dir_ids.binary_search(&entry.id).is_err() + && entry_iter.advance_to_sibling() + { + continue; + } + entry_iter.advance(); + } + + snapshot.propagate_git_statuses(&mut visible_worktree_entries); + + visible_worktree_entries.sort_by(|entry_a, entry_b| { + let mut components_a = entry_a.path.components().peekable(); + let mut components_b = entry_b.path.components().peekable(); + loop { + match (components_a.next(), components_b.next()) { + (Some(component_a), Some(component_b)) => { + let a_is_file = components_a.peek().is_none() && entry_a.is_file(); + let b_is_file = components_b.peek().is_none() && entry_b.is_file(); + let ordering = a_is_file.cmp(&b_is_file).then_with(|| { + let name_a = + UniCase::new(component_a.as_os_str().to_string_lossy()); + let name_b = + UniCase::new(component_b.as_os_str().to_string_lossy()); + name_a.cmp(&name_b) + }); + if !ordering.is_eq() { + return ordering; + } + } + (Some(_), None) => break Ordering::Greater, + (None, Some(_)) => break Ordering::Less, + (None, None) => break Ordering::Equal, + } + } + }); + self.visible_entries + .push((worktree_id, visible_worktree_entries)); + } + + if let Some((worktree_id, entry_id)) = new_selected_entry { + self.selection = Some(Selection { + worktree_id, + entry_id, + }); + } + } + + fn expand_entry( + &mut self, + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) { + self.project.update(cx, |project, cx| { + if let Some((worktree, expanded_dir_ids)) = project + .worktree_for_id(worktree_id, cx) + .zip(self.expanded_dir_ids.get_mut(&worktree_id)) + { + project.expand_entry(worktree_id, entry_id, cx); + let worktree = worktree.read(cx); + + if let Some(mut entry) = worktree.entry_for_id(entry_id) { + loop { + if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(ix, entry.id); + } + + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } + } + } + } + }); + } + + fn for_each_visible_entry( + &self, + range: Range, + cx: &mut ViewContext, + mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext), + ) { + let mut ix = 0; + for (worktree_id, visible_worktree_entries) in &self.visible_entries { + if ix >= range.end { + return; + } + + if ix + visible_worktree_entries.len() <= range.start { + ix += visible_worktree_entries.len(); + continue; + } + + let end_ix = range.end.min(ix + visible_worktree_entries.len()); + let (git_status_setting, show_file_icons, show_folder_icons) = { + let settings = ProjectPanelSettings::get_global(cx); + ( + settings.git_status, + settings.file_icons, + settings.folder_icons, + ) + }; + if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { + let snapshot = worktree.read(cx).snapshot(); + let root_name = OsStr::new(snapshot.root_name()); + let expanded_entry_ids = self + .expanded_dir_ids + .get(&snapshot.id()) + .map(Vec::as_slice) + .unwrap_or(&[]); + + let entry_range = range.start.saturating_sub(ix)..end_ix - ix; + for entry in visible_worktree_entries[entry_range].iter() { + let status = git_status_setting.then(|| entry.git_status).flatten(); + let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); + let icon = match entry.kind { + EntryKind::File(_) => { + if show_file_icons { + Some(FileAssociations::get_icon(&entry.path, cx)) + } else { + None + } + } + _ => { + if show_folder_icons { + Some(FileAssociations::get_folder_icon(is_expanded, cx)) + } else { + Some(FileAssociations::get_chevron_icon(is_expanded, cx)) + } + } + }; + + let mut details = EntryDetails { + filename: entry + .path + .file_name() + .unwrap_or(root_name) + .to_string_lossy() + .to_string(), + icon, + path: entry.path.clone(), + depth: entry.path.components().count(), + kind: entry.kind, + is_ignored: entry.is_ignored, + is_expanded, + is_selected: self.selection.map_or(false, |e| { + e.worktree_id == snapshot.id() && e.entry_id == entry.id + }), + is_editing: false, + is_processing: false, + is_cut: self + .clipboard_entry + .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), + git_status: status, + }; + + if let Some(edit_state) = &self.edit_state { + let is_edited_entry = if edit_state.is_new_entry { + entry.id == NEW_ENTRY_ID + } else { + entry.id == edit_state.entry_id + }; + + if is_edited_entry { + if let Some(processing_filename) = &edit_state.processing_filename { + details.is_processing = true; + details.filename.clear(); + details.filename.push_str(processing_filename); + } else { + if edit_state.is_new_entry { + details.filename.clear(); + } + details.is_editing = true; + } + } + } + + callback(entry.id, details, cx); + } + } + ix = end_ix; + } + } + + fn render_entry_visual_element( + details: &EntryDetails, + editor: Option<&View>, + padding: Pixels, + cx: &mut ViewContext, + ) -> Div { + let show_editor = details.is_editing && !details.is_processing; + + let theme = cx.theme(); + let filename_text_color = details + .git_status + .as_ref() + .map(|status| match status { + GitFileStatus::Added => theme.styles.status.created, + GitFileStatus::Modified => theme.styles.status.modified, + GitFileStatus::Conflict => theme.styles.status.conflict, + }) + .unwrap_or(theme.styles.status.info); + + h_stack() + .child(if let Some(icon) = &details.icon { + div().child(svg().path(icon.to_string())) + } else { + div() + }) + .child( + if let (Some(editor), true) = (editor, show_editor) { + div().child(editor.clone()) + } else { + div().child(details.filename.clone()) + } + .ml_1(), + ) + .pl(padding) + } + + fn render_entry( + entry_id: ProjectEntryId, + details: EntryDetails, + editor: &View, + // dragged_entry_destination: &mut Option>, + // theme: &theme::ProjectPanel, + cx: &mut ViewContext, + ) -> Div> { + let kind = details.kind; + let settings = ProjectPanelSettings::get_global(cx); + const INDENT_SIZE: Pixels = px(16.0); + let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); + let show_editor = details.is_editing && !details.is_processing; + + Self::render_entry_visual_element(&details, Some(editor), padding, cx) + .id(entry_id.to_proto() as usize) + .on_click(move |this, event, cx| { + if !show_editor { + if kind.is_dir() { + this.toggle_expanded(entry_id, cx); + } else { + if event.down.modifiers.command { + this.split_entry(entry_id, cx); + } else { + this.open_entry(entry_id, event.up.click_count > 1, cx); + } + } + } + }) + // .on_down(MouseButton::Right, move |event, this, cx| { + // this.deploy_context_menu(event.position, entry_id, cx); + // }) + // .on_up(MouseButton::Left, move |_, this, cx| { + // if let Some((_, dragged_entry)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // this.move_entry( + // *dragged_entry, + // entry_id, + // matches!(details.kind, EntryKind::File(_)), + // cx, + // ); + // } + // }) + } +} + +impl Render for ProjectPanel { + type Element = Div, FocusEnabled>; + + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { + enum ProjectPanel {} + let theme = cx.theme(); + let last_worktree_root_id = self.last_worktree_root_id; + + let has_worktree = self.visible_entries.len() != 0; + + if has_worktree { + div() + .id("project-panel") + .track_focus(&self.focus_handle) + .child( + uniform_list( + "entries", + self.visible_entries + .iter() + .map(|(_, worktree_entries)| worktree_entries.len()) + .sum(), + |this: &mut Self, range, cx| { + let mut items = SmallVec::new(); + this.for_each_visible_entry(range, cx, |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + // &mut dragged_entry_destination, + cx, + )); + }); + items + }, + ) + .track_scroll(self.list.clone()), + ) + } else { + v_stack() + .id("empty-project_panel") + .track_focus(&self.focus_handle) + } + } +} + +impl EventEmitter for ProjectPanel {} + +impl EventEmitter for ProjectPanel {} + +impl workspace::dock::Panel for ProjectPanel { + fn position(&self, cx: &WindowContext) -> DockPosition { + match ProjectPanelSettings::get_global(cx).dock { + ProjectPanelDockPosition::Left => DockPosition::Left, + ProjectPanelDockPosition::Right => DockPosition::Right, + } + } + + fn position_is_valid(&self, position: DockPosition) -> bool { + matches!(position, DockPosition::Left | DockPosition::Right) + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings| { + let dock = match position { + DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left, + DockPosition::Right => ProjectPanelDockPosition::Right, + }; + settings.dock = Some(dock); + }, + ); + } + + fn size(&self, cx: &WindowContext) -> f32 { + self.width + .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width) + } + + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + self.width = size; + self.serialize(cx); + cx.notify(); + } + + fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { + Some("icons/project.svg") + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Project Panel".into(), Some(Box::new(ToggleFocus))) + } + + // fn should_change_position_on_event(event: &Self::Event) -> bool { + // matches!(event, Event::DockPositionChanged) + // } + + fn has_focus(&self, _: &WindowContext) -> bool { + self.has_focus + } + + fn persistent_name(&self) -> &'static str { + "Project Panel" + } + + fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle { + self.focus_handle.clone() + } + + // fn is_focus_event(event: &Self::Event) -> bool { + // matches!(event, Event::Focus) + // } +} + +impl ClipboardEntry { + fn is_cut(&self) -> bool { + matches!(self, Self::Cut { .. }) + } + + fn entry_id(&self) -> ProjectEntryId { + match self { + ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => { + *entry_id + } + } + } + + fn worktree_id(&self) -> WorktreeId { + match self { + ClipboardEntry::Copied { worktree_id, .. } + | ClipboardEntry::Cut { worktree_id, .. } => *worktree_id, + } + } +} + +// todo!() +// #[cfg(test)] +// mod tests { +// use super::*; +// use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle}; +// use pretty_assertions::assert_eq; +// use project::FakeFs; +// use serde_json::json; +// use settings::SettingsStore; +// use std::{ +// collections::HashSet, +// path::{Path, PathBuf}, +// sync::atomic::{self, AtomicUsize}, +// }; +// use workspace::{pane, AppState}; + +// #[gpui::test] +// async fn test_visible_list(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.executor().clone()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// toggle_expand_dir(&panel, "root1/b", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b <== selected", +// " > 3", +// " > 4", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// assert_eq!( +// visible_entries_as_strings(&panel, 6..9, cx), +// &[ +// // +// " > C", +// " .dockerignore", +// "v root2", +// ] +// ); +// } + +// #[gpui::test(iterations = 30)] +// async fn test_editing_files(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// select_path(&panel, "root1", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1 <== selected", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// // Add a file with the root folder selected. The filename editor is placed +// // before the first file in the root folder. +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [EDITOR: ''] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); +// panel.confirm(&Confirm, cx).unwrap() +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [PROCESSING: 'the-new-filename'] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// " the-new-filename <== selected", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// select_path(&panel, "root1/b", cx); +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: ''] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel +// .update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx)); +// panel.confirm(&Confirm, cx).unwrap() +// }) +// .await +// .unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " another-filename.txt <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// select_path(&panel, "root1/b/another-filename.txt", cx); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: 'another-filename.txt'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// let file_name_selections = editor.selections.all::(cx); +// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); +// let file_name_selection = &file_name_selections[0]; +// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); +// assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension"); + +// editor.set_text("a-different-filename.tar.gz", cx) +// }); +// panel.confirm(&Confirm, cx).unwrap() +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [PROCESSING: 'a-different-filename.tar.gz'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " a-different-filename.tar.gz <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: 'a-different-filename.tar.gz'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// let file_name_selections = editor.selections.all::(cx); +// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); +// let file_name_selection = &file_name_selections[0]; +// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); +// assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot"); + +// }); +// panel.cancel(&Cancel, cx) +// }); + +// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > [EDITOR: ''] <== selected", +// " > 3", +// " > 4", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("new-dir", cx)); +// panel.confirm(&Confirm, cx).unwrap() +// }); +// panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > [PROCESSING: 'new-dir']", +// " > 3 <== selected", +// " > 4", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3 <== selected", +// " > 4", +// " > new-dir", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > [EDITOR: '3'] <== selected", +// " > 4", +// " > new-dir", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// // Dismiss the rename editor when it loses focus. +// workspace.update(cx, |_, cx| cx.focus_self()); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3 <== selected", +// " > 4", +// " > new-dir", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); +// } + +// #[gpui::test(iterations = 30)] +// async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// select_path(&panel, "root1", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1 <== selected", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// // Add a file with the root folder selected. The filename editor is placed +// // before the first file in the root folder. +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [EDITOR: ''] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// editor.set_text("/bdir1/dir2/the-new-filename", cx) +// }); +// panel.confirm(&Confirm, cx).unwrap() +// }); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..13, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " v bdir1", +// " v dir2", +// " the-new-filename <== selected", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); +// } + +// #[gpui::test] +// async fn test_copy_paste(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// "one.two.txt": "", +// "one.txt": "" +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// panel.update(cx, |panel, cx| { +// panel.select_next(&Default::default(), cx); +// panel.select_next(&Default::default(), cx); +// }); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// // +// "v root1", +// " one.two.txt <== selected", +// " one.txt", +// ] +// ); + +// // Regression test - file name is created correctly when +// // the copied file's name contains multiple dots. +// panel.update(cx, |panel, cx| { +// panel.copy(&Default::default(), cx); +// panel.paste(&Default::default(), cx); +// }); +// cx.foreground().run_until_parked(); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// // +// "v root1", +// " one.two copy.txt", +// " one.two.txt <== selected", +// " one.txt", +// ] +// ); + +// panel.update(cx, |panel, cx| { +// panel.paste(&Default::default(), cx); +// }); +// cx.foreground().run_until_parked(); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// // +// "v root1", +// " one.two copy 1.txt", +// " one.two copy.txt", +// " one.two.txt <== selected", +// " one.txt", +// ] +// ); +// } + +// #[gpui::test] +// async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// toggle_expand_dir(&panel, "src/test", cx); +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ] +// ); +// ensure_single_file_is_opened(window, "test/first.rs", cx); + +// submit_deletion(window.into(), &panel, cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " second.rs", +// " third.rs" +// ], +// "Project panel should have no deleted file, no other file is selected in it" +// ); +// ensure_no_open_items_and_panes(window.into(), &workspace, cx); + +// select_path(&panel, "src/test/second.rs", cx); +// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " second.rs <== selected", +// " third.rs" +// ] +// ); +// ensure_single_file_is_opened(window, "test/second.rs", cx); + +// window.update(cx, |cx| { +// let active_items = workspace +// .read(cx) +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()) +// .collect::>(); +// assert_eq!(active_items.len(), 1); +// let open_editor = active_items +// .into_iter() +// .next() +// .unwrap() +// .downcast::() +// .expect("Open item should be an editor"); +// open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); +// }); +// submit_deletion(window.into(), &panel, cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " v test", " third.rs"], +// "Project panel should have no deleted file, with one last file remaining" +// ); +// ensure_no_open_items_and_panes(window.into(), &workspace, cx); +// } + +// #[gpui::test] +// async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// select_path(&panel, "src/", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src <== selected", " > test"] +// ); +// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " > [EDITOR: ''] <== selected", " > test"] +// ); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("test", cx)); +// assert!( +// panel.confirm(&Confirm, cx).is_none(), +// "Should not allow to confirm on conflicting new directory name" +// ) +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " > test"], +// "File list should be unchanged after failed folder create confirmation" +// ); + +// select_path(&panel, "src/test/", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " > test <== selected"] +// ); +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " [EDITOR: ''] <== selected", +// " first.rs", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("first.rs", cx)); +// assert!( +// panel.confirm(&Confirm, cx).is_none(), +// "Should not allow to confirm on conflicting new file name" +// ) +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs", +// " second.rs", +// " third.rs" +// ], +// "File list should be unchanged after failed file create confirmation" +// ); + +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ], +// ); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " [EDITOR: 'first.rs'] <== selected", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("second.rs", cx)); +// assert!( +// panel.confirm(&Confirm, cx).is_none(), +// "Should not allow to confirm on conflicting file rename" +// ) +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ], +// "File list should be unchanged after failed rename confirmation" +// ); +// } + +// #[gpui::test] +// async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// let new_search_events_count = Arc::new(AtomicUsize::new(0)); +// let _subscription = panel.update(cx, |_, cx| { +// let subcription_count = Arc::clone(&new_search_events_count); +// cx.subscribe(&cx.handle(), move |_, _, event, _| { +// if matches!(event, Event::NewSearchInDirectory { .. }) { +// subcription_count.fetch_add(1, atomic::Ordering::SeqCst); +// } +// }) +// }); + +// toggle_expand_dir(&panel, "src/test", cx); +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel.new_search_in_directory(&NewSearchInDirectory, cx) +// }); +// assert_eq!( +// new_search_events_count.load(atomic::Ordering::SeqCst), +// 0, +// "Should not trigger new search in directory when called on a file" +// ); + +// select_path(&panel, "src/test", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test <== selected", +// " first.rs", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel.new_search_in_directory(&NewSearchInDirectory, cx) +// }); +// assert_eq!( +// new_search_events_count.load(atomic::Ordering::SeqCst), +// 1, +// "Should trigger new search in directory when called on a directory" +// ); +// } + +// #[gpui::test] +// async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/project_root", +// json!({ +// "dir_1": { +// "nested_dir": { +// "file_a.py": "# File contents", +// "file_b.py": "# File contents", +// "file_c.py": "# File contents", +// }, +// "file_1.py": "# File contents", +// "file_2.py": "# File contents", +// "file_3.py": "# File contents", +// }, +// "dir_2": { +// "file_1.py": "# File contents", +// "file_2.py": "# File contents", +// "file_3.py": "# File contents", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// panel.update(cx, |panel, cx| { +// panel.collapse_all_entries(&CollapseAllEntries, cx) +// }); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v project_root", " > dir_1", " > dir_2",] +// ); + +// // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries +// toggle_expand_dir(&panel, "project_root/dir_1", cx); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v project_root", +// " v dir_1 <== selected", +// " > nested_dir", +// " file_1.py", +// " file_2.py", +// " file_3.py", +// " > dir_2", +// ] +// ); +// } + +// #[gpui::test] +// async fn test_new_file_move(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.as_fake().insert_tree("/root", json!({})).await; +// let project = Project::test(fs, ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// // Make a new buffer with no backing file +// workspace.update(cx, |workspace, cx| { +// Editor::new_file(workspace, &Default::default(), cx) +// }); + +// // "Save as"" the buffer, creating a new backing file for it +// let task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(workspace::SaveIntent::Save, cx) +// }); + +// cx.foreground().run_until_parked(); +// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); +// task.await.unwrap(); + +// // Rename the file +// select_path(&panel, "root/new", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v root", " new <== selected"] +// ); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("newer", cx)); +// }); +// panel +// .update(cx, |panel, cx| panel.confirm(&Confirm, cx)) +// .unwrap() +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v root", " newer <== selected"] +// ); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.save_active_item(workspace::SaveIntent::Save, cx) +// }) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); +// // assert that saving the file doesn't restore "new" +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v root", " newer <== selected"] +// ); +// } + +// fn toggle_expand_dir( +// panel: &View, +// path: impl AsRef, +// cx: &mut TestAppContext, +// ) { +// let path = path.as_ref(); +// panel.update(cx, |panel, cx| { +// for worktree in panel.project.read(cx).worktrees().collect::>() { +// let worktree = worktree.read(cx); +// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { +// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; +// panel.toggle_expanded(entry_id, cx); +// return; +// } +// } +// panic!("no worktree for path {:?}", path); +// }); +// } + +// fn select_path(panel: &View, path: impl AsRef, cx: &mut TestAppContext) { +// let path = path.as_ref(); +// panel.update(cx, |panel, cx| { +// for worktree in panel.project.read(cx).worktrees().collect::>() { +// let worktree = worktree.read(cx); +// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { +// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; +// panel.selection = Some(Selection { +// worktree_id: worktree.id(), +// entry_id, +// }); +// return; +// } +// } +// panic!("no worktree for path {:?}", path); +// }); +// } + +// fn visible_entries_as_strings( +// panel: &View, +// range: Range, +// cx: &mut TestAppContext, +// ) -> Vec { +// let mut result = Vec::new(); +// let mut project_entries = HashSet::new(); +// let mut has_editor = false; + +// panel.update(cx, |panel, cx| { +// panel.for_each_visible_entry(range, cx, |project_entry, details, _| { +// if details.is_editing { +// assert!(!has_editor, "duplicate editor entry"); +// has_editor = true; +// } else { +// assert!( +// project_entries.insert(project_entry), +// "duplicate project entry {:?} {:?}", +// project_entry, +// details +// ); +// } + +// let indent = " ".repeat(details.depth); +// let icon = if details.kind.is_dir() { +// if details.is_expanded { +// "v " +// } else { +// "> " +// } +// } else { +// " " +// }; +// let name = if details.is_editing { +// format!("[EDITOR: '{}']", details.filename) +// } else if details.is_processing { +// format!("[PROCESSING: '{}']", details.filename) +// } else { +// details.filename.clone() +// }; +// let selected = if details.is_selected { +// " <== selected" +// } else { +// "" +// }; +// result.push(format!("{indent}{icon}{name}{selected}")); +// }); +// }); + +// result +// } + +// fn init_test(cx: &mut TestAppContext) { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// init_settings(cx); +// theme::init(cx); +// language::init(cx); +// editor::init_settings(cx); +// crate::init((), cx); +// workspace::init_settings(cx); +// client::init_settings(cx); +// Project::init_settings(cx); +// }); +// } + +// fn init_test_with_editor(cx: &mut TestAppContext) { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// let app_state = AppState::test(cx); +// theme::init(cx); +// init_settings(cx); +// language::init(cx); +// editor::init(cx); +// pane::init(cx); +// crate::init((), cx); +// workspace::init(app_state.clone(), cx); +// Project::init_settings(cx); +// }); +// } + +// fn ensure_single_file_is_opened( +// window: WindowHandle, +// expected_path: &str, +// cx: &mut TestAppContext, +// ) { +// window.update_root(cx, |workspace, cx| { +// let worktrees = workspace.worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// let worktree_id = WorktreeId::from_usize(worktrees[0].id()); + +// let open_project_paths = workspace +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) +// .collect::>(); +// assert_eq!( +// open_project_paths, +// vec![ProjectPath { +// worktree_id, +// path: Arc::from(Path::new(expected_path)) +// }], +// "Should have opened file, selected in project panel" +// ); +// }); +// } + +// fn submit_deletion( +// window: AnyWindowHandle, +// panel: &View, +// cx: &mut TestAppContext, +// ) { +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts before the deletion" +// ); +// panel.update(cx, |panel, cx| { +// panel +// .delete(&Delete, cx) +// .expect("Deletion start") +// .detach_and_log_err(cx); +// }); +// assert!( +// window.has_pending_prompt(cx), +// "Should have a prompt after the deletion" +// ); +// window.simulate_prompt_answer(0, cx); +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts after prompt was replied to" +// ); +// cx.foreground().run_until_parked(); +// } + +// fn ensure_no_open_items_and_panes( +// window: AnyWindowHandle, +// workspace: &View, +// cx: &mut TestAppContext, +// ) { +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts after deletion operation closes the file" +// ); +// window.read_with(cx, |cx| { +// let open_project_paths = workspace +// .read(cx) +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) +// .collect::>(); +// assert!( +// open_project_paths.is_empty(), +// "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" +// ); +// }); +// } +// } diff --git a/crates/rpc2/src/peer.rs b/crates/rpc2/src/peer.rs index 80a2ab4378dfb9fcd4db41d2f751ab94957260d1..20a36efdfe1c1ae59fd9130668c7afda0b1f792e 100644 --- a/crates/rpc2/src/peer.rs +++ b/crates/rpc2/src/peer.rs @@ -577,18 +577,18 @@ mod tests { let client2 = Peer::new(0); let (client1_to_server_conn, server_to_client_1_conn, _kill) = - Connection::in_memory(cx.executor().clone()); + Connection::in_memory(cx.executor()); let (client1_conn_id, io_task1, client1_incoming) = - client1.add_test_connection(client1_to_server_conn, cx.executor().clone()); + client1.add_test_connection(client1_to_server_conn, cx.executor()); let (_, io_task2, server_incoming1) = - server.add_test_connection(server_to_client_1_conn, cx.executor().clone()); + server.add_test_connection(server_to_client_1_conn, cx.executor()); let (client2_to_server_conn, server_to_client_2_conn, _kill) = - Connection::in_memory(cx.executor().clone()); + Connection::in_memory(cx.executor()); let (client2_conn_id, io_task3, client2_incoming) = - client2.add_test_connection(client2_to_server_conn, cx.executor().clone()); + client2.add_test_connection(client2_to_server_conn, cx.executor()); let (_, io_task4, server_incoming2) = - server.add_test_connection(server_to_client_2_conn, cx.executor().clone()); + server.add_test_connection(server_to_client_2_conn, cx.executor()); executor.spawn(io_task1).detach(); executor.spawn(io_task2).detach(); @@ -763,16 +763,16 @@ mod tests { #[gpui::test(iterations = 50)] async fn test_dropping_request_before_completion(cx: &mut TestAppContext) { - let executor = cx.executor().clone(); + let executor = cx.executor(); let server = Peer::new(0); let client = Peer::new(0); let (client_to_server_conn, server_to_client_conn, _kill) = - Connection::in_memory(cx.executor().clone()); + Connection::in_memory(cx.executor()); let (client_to_server_conn_id, io_task1, mut client_incoming) = - client.add_test_connection(client_to_server_conn, cx.executor().clone()); + client.add_test_connection(client_to_server_conn, cx.executor()); let (server_to_client_conn_id, io_task2, mut server_incoming) = - server.add_test_connection(server_to_client_conn, cx.executor().clone()); + server.add_test_connection(server_to_client_conn, cx.executor()); executor.spawn(io_task1).detach(); executor.spawn(io_task2).detach(); diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index f0ba124162d546bcdbdc1fce12e986364f950dad..c4c1d75eac26b245ede5b79b6042e33cbeec67f7 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -48,7 +48,7 @@ fn main() { let args = Args::parse(); let story_selector = args.story.clone(); - let theme_name = args.theme.unwrap_or("Zed Pro Moonlight".to_string()); + let theme_name = args.theme.unwrap_or("One Dark".to_string()); let asset_source = Arc::new(Assets); gpui::App::production(asset_source).run(move |cx| { diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 6cfda37a2a632a2d7228152409c1deb1c00c2aa5..91efecbfb310103deeafb683becf31b9b7732c42 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -1,261 +1,15 @@ -use gpui::{hsla, Hsla, Rgba}; +use gpui::{Hsla, Rgba}; -use crate::colors::{StatusColors, SystemColors, ThemeColors}; use crate::scale::{ColorScaleSet, ColorScales}; -use crate::syntax::SyntaxTheme; -use crate::{ColorScale, PlayerColor, PlayerColors}; +use crate::ColorScale; +use crate::{SystemColors, ThemeColors}; -impl Default for PlayerColors { - fn default() -> Self { - Self(vec![ - PlayerColor { - cursor: blue().dark().step_9(), - background: blue().dark().step_5(), - selection: blue().dark().step_3(), - }, - PlayerColor { - cursor: orange().dark().step_9(), - background: orange().dark().step_5(), - selection: orange().dark().step_3(), - }, - PlayerColor { - cursor: pink().dark().step_9(), - background: pink().dark().step_5(), - selection: pink().dark().step_3(), - }, - PlayerColor { - cursor: lime().dark().step_9(), - background: lime().dark().step_5(), - selection: lime().dark().step_3(), - }, - PlayerColor { - cursor: purple().dark().step_9(), - background: purple().dark().step_5(), - selection: purple().dark().step_3(), - }, - PlayerColor { - cursor: amber().dark().step_9(), - background: amber().dark().step_5(), - selection: amber().dark().step_3(), - }, - PlayerColor { - cursor: jade().dark().step_9(), - background: jade().dark().step_5(), - selection: jade().dark().step_3(), - }, - PlayerColor { - cursor: red().dark().step_9(), - background: red().dark().step_5(), - selection: red().dark().step_3(), - }, - ]) - } -} - -impl PlayerColors { - pub fn default_light() -> Self { - Self(vec![ - PlayerColor { - cursor: blue().light().step_9(), - background: blue().light().step_4(), - selection: blue().light().step_3(), - }, - PlayerColor { - cursor: orange().light().step_9(), - background: orange().light().step_4(), - selection: orange().light().step_3(), - }, - PlayerColor { - cursor: pink().light().step_9(), - background: pink().light().step_4(), - selection: pink().light().step_3(), - }, - PlayerColor { - cursor: lime().light().step_9(), - background: lime().light().step_4(), - selection: lime().light().step_3(), - }, - PlayerColor { - cursor: purple().light().step_9(), - background: purple().light().step_4(), - selection: purple().light().step_3(), - }, - PlayerColor { - cursor: amber().light().step_9(), - background: amber().light().step_4(), - selection: amber().light().step_3(), - }, - PlayerColor { - cursor: jade().light().step_9(), - background: jade().light().step_4(), - selection: jade().light().step_3(), - }, - PlayerColor { - cursor: red().light().step_9(), - background: red().light().step_4(), - selection: red().light().step_3(), - }, - ]) - } -} - -fn neutral() -> ColorScaleSet { +pub(crate) fn neutral() -> ColorScaleSet { slate() } -impl Default for SystemColors { - fn default() -> Self { - Self { - transparent: hsla(0.0, 0.0, 0.0, 0.0), - mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0), - mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0), - mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0), - } - } -} - -impl Default for StatusColors { - fn default() -> Self { - Self { - conflict: red().dark().step_9(), - created: grass().dark().step_9(), - deleted: red().dark().step_9(), - error: red().dark().step_9(), - hidden: neutral().dark().step_9(), - ignored: neutral().dark().step_9(), - info: blue().dark().step_9(), - modified: yellow().dark().step_9(), - renamed: blue().dark().step_9(), - success: grass().dark().step_9(), - warning: yellow().dark().step_9(), - } - } -} - -impl SyntaxTheme { - pub fn default_light() -> Self { - Self { - highlights: vec![ - ("attribute".into(), cyan().light().step_11().into()), - ("boolean".into(), tomato().light().step_11().into()), - ("comment".into(), neutral().light().step_11().into()), - ("comment.doc".into(), iris().light().step_12().into()), - ("constant".into(), red().light().step_9().into()), - ("constructor".into(), red().light().step_9().into()), - ("embedded".into(), red().light().step_9().into()), - ("emphasis".into(), red().light().step_9().into()), - ("emphasis.strong".into(), red().light().step_9().into()), - ("enum".into(), red().light().step_9().into()), - ("function".into(), red().light().step_9().into()), - ("hint".into(), red().light().step_9().into()), - ("keyword".into(), orange().light().step_11().into()), - ("label".into(), red().light().step_9().into()), - ("link_text".into(), red().light().step_9().into()), - ("link_uri".into(), red().light().step_9().into()), - ("number".into(), red().light().step_9().into()), - ("operator".into(), red().light().step_9().into()), - ("predictive".into(), red().light().step_9().into()), - ("preproc".into(), red().light().step_9().into()), - ("primary".into(), red().light().step_9().into()), - ("property".into(), red().light().step_9().into()), - ("punctuation".into(), neutral().light().step_11().into()), - ( - "punctuation.bracket".into(), - neutral().light().step_11().into(), - ), - ( - "punctuation.delimiter".into(), - neutral().light().step_11().into(), - ), - ( - "punctuation.list_marker".into(), - blue().light().step_11().into(), - ), - ("punctuation.special".into(), red().light().step_9().into()), - ("string".into(), jade().light().step_11().into()), - ("string.escape".into(), red().light().step_9().into()), - ("string.regex".into(), tomato().light().step_11().into()), - ("string.special".into(), red().light().step_9().into()), - ( - "string.special.symbol".into(), - red().light().step_9().into(), - ), - ("tag".into(), red().light().step_9().into()), - ("text.literal".into(), red().light().step_9().into()), - ("title".into(), red().light().step_9().into()), - ("type".into(), red().light().step_9().into()), - ("variable".into(), red().light().step_9().into()), - ("variable.special".into(), red().light().step_9().into()), - ("variant".into(), red().light().step_9().into()), - ], - inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style") - suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style") - } - } - - pub fn default_dark() -> Self { - Self { - highlights: vec![ - ("attribute".into(), tomato().dark().step_11().into()), - ("boolean".into(), tomato().dark().step_11().into()), - ("comment".into(), neutral().dark().step_11().into()), - ("comment.doc".into(), iris().dark().step_12().into()), - ("constant".into(), orange().dark().step_11().into()), - ("constructor".into(), gold().dark().step_11().into()), - ("embedded".into(), red().dark().step_11().into()), - ("emphasis".into(), red().dark().step_11().into()), - ("emphasis.strong".into(), red().dark().step_11().into()), - ("enum".into(), yellow().dark().step_11().into()), - ("function".into(), blue().dark().step_11().into()), - ("hint".into(), indigo().dark().step_11().into()), - ("keyword".into(), plum().dark().step_11().into()), - ("label".into(), red().dark().step_11().into()), - ("link_text".into(), red().dark().step_11().into()), - ("link_uri".into(), red().dark().step_11().into()), - ("number".into(), red().dark().step_11().into()), - ("operator".into(), red().dark().step_11().into()), - ("predictive".into(), red().dark().step_11().into()), - ("preproc".into(), red().dark().step_11().into()), - ("primary".into(), red().dark().step_11().into()), - ("property".into(), red().dark().step_11().into()), - ("punctuation".into(), neutral().dark().step_11().into()), - ( - "punctuation.bracket".into(), - neutral().dark().step_11().into(), - ), - ( - "punctuation.delimiter".into(), - neutral().dark().step_11().into(), - ), - ( - "punctuation.list_marker".into(), - blue().dark().step_11().into(), - ), - ("punctuation.special".into(), red().dark().step_11().into()), - ("string".into(), lime().dark().step_11().into()), - ("string.escape".into(), orange().dark().step_11().into()), - ("string.regex".into(), tomato().dark().step_11().into()), - ("string.special".into(), red().dark().step_11().into()), - ( - "string.special.symbol".into(), - red().dark().step_11().into(), - ), - ("tag".into(), red().dark().step_11().into()), - ("text.literal".into(), purple().dark().step_11().into()), - ("title".into(), sky().dark().step_11().into()), - ("type".into(), mint().dark().step_11().into()), - ("variable".into(), red().dark().step_11().into()), - ("variable.special".into(), red().dark().step_11().into()), - ("variant".into(), red().dark().step_11().into()), - ], - inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style") - suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style") - } - } -} - impl ThemeColors { - pub fn default_light() -> Self { + pub fn light() -> Self { let system = SystemColors::default(); Self { @@ -327,7 +81,7 @@ impl ThemeColors { } } - pub fn default_dark() -> Self { + pub fn dark() -> Self { let system = SystemColors::default(); Self { @@ -470,7 +224,7 @@ pub fn default_color_scales() -> ColorScales { } } -fn gray() -> ColorScaleSet { +pub(crate) fn gray() -> ColorScaleSet { StaticColorScaleSet { scale: "Gray", light: [ @@ -534,7 +288,7 @@ fn gray() -> ColorScaleSet { .unwrap() } -fn mauve() -> ColorScaleSet { +pub(crate) fn mauve() -> ColorScaleSet { StaticColorScaleSet { scale: "Mauve", light: [ @@ -598,7 +352,7 @@ fn mauve() -> ColorScaleSet { .unwrap() } -fn slate() -> ColorScaleSet { +pub(crate) fn slate() -> ColorScaleSet { StaticColorScaleSet { scale: "Slate", light: [ @@ -662,7 +416,7 @@ fn slate() -> ColorScaleSet { .unwrap() } -fn sage() -> ColorScaleSet { +pub(crate) fn sage() -> ColorScaleSet { StaticColorScaleSet { scale: "Sage", light: [ @@ -726,7 +480,7 @@ fn sage() -> ColorScaleSet { .unwrap() } -fn olive() -> ColorScaleSet { +pub(crate) fn olive() -> ColorScaleSet { StaticColorScaleSet { scale: "Olive", light: [ @@ -790,7 +544,7 @@ fn olive() -> ColorScaleSet { .unwrap() } -fn sand() -> ColorScaleSet { +pub(crate) fn sand() -> ColorScaleSet { StaticColorScaleSet { scale: "Sand", light: [ @@ -854,7 +608,7 @@ fn sand() -> ColorScaleSet { .unwrap() } -fn gold() -> ColorScaleSet { +pub(crate) fn gold() -> ColorScaleSet { StaticColorScaleSet { scale: "Gold", light: [ @@ -918,7 +672,7 @@ fn gold() -> ColorScaleSet { .unwrap() } -fn bronze() -> ColorScaleSet { +pub(crate) fn bronze() -> ColorScaleSet { StaticColorScaleSet { scale: "Bronze", light: [ @@ -982,7 +736,7 @@ fn bronze() -> ColorScaleSet { .unwrap() } -fn brown() -> ColorScaleSet { +pub(crate) fn brown() -> ColorScaleSet { StaticColorScaleSet { scale: "Brown", light: [ @@ -1046,7 +800,7 @@ fn brown() -> ColorScaleSet { .unwrap() } -fn yellow() -> ColorScaleSet { +pub(crate) fn yellow() -> ColorScaleSet { StaticColorScaleSet { scale: "Yellow", light: [ @@ -1110,7 +864,7 @@ fn yellow() -> ColorScaleSet { .unwrap() } -fn amber() -> ColorScaleSet { +pub(crate) fn amber() -> ColorScaleSet { StaticColorScaleSet { scale: "Amber", light: [ @@ -1174,7 +928,7 @@ fn amber() -> ColorScaleSet { .unwrap() } -fn orange() -> ColorScaleSet { +pub(crate) fn orange() -> ColorScaleSet { StaticColorScaleSet { scale: "Orange", light: [ @@ -1238,7 +992,7 @@ fn orange() -> ColorScaleSet { .unwrap() } -fn tomato() -> ColorScaleSet { +pub(crate) fn tomato() -> ColorScaleSet { StaticColorScaleSet { scale: "Tomato", light: [ @@ -1302,7 +1056,7 @@ fn tomato() -> ColorScaleSet { .unwrap() } -fn red() -> ColorScaleSet { +pub(crate) fn red() -> ColorScaleSet { StaticColorScaleSet { scale: "Red", light: [ @@ -1366,7 +1120,7 @@ fn red() -> ColorScaleSet { .unwrap() } -fn ruby() -> ColorScaleSet { +pub(crate) fn ruby() -> ColorScaleSet { StaticColorScaleSet { scale: "Ruby", light: [ @@ -1430,7 +1184,7 @@ fn ruby() -> ColorScaleSet { .unwrap() } -fn crimson() -> ColorScaleSet { +pub(crate) fn crimson() -> ColorScaleSet { StaticColorScaleSet { scale: "Crimson", light: [ @@ -1494,7 +1248,7 @@ fn crimson() -> ColorScaleSet { .unwrap() } -fn pink() -> ColorScaleSet { +pub(crate) fn pink() -> ColorScaleSet { StaticColorScaleSet { scale: "Pink", light: [ @@ -1558,7 +1312,7 @@ fn pink() -> ColorScaleSet { .unwrap() } -fn plum() -> ColorScaleSet { +pub(crate) fn plum() -> ColorScaleSet { StaticColorScaleSet { scale: "Plum", light: [ @@ -1622,7 +1376,7 @@ fn plum() -> ColorScaleSet { .unwrap() } -fn purple() -> ColorScaleSet { +pub(crate) fn purple() -> ColorScaleSet { StaticColorScaleSet { scale: "Purple", light: [ @@ -1686,7 +1440,7 @@ fn purple() -> ColorScaleSet { .unwrap() } -fn violet() -> ColorScaleSet { +pub(crate) fn violet() -> ColorScaleSet { StaticColorScaleSet { scale: "Violet", light: [ @@ -1750,7 +1504,7 @@ fn violet() -> ColorScaleSet { .unwrap() } -fn iris() -> ColorScaleSet { +pub(crate) fn iris() -> ColorScaleSet { StaticColorScaleSet { scale: "Iris", light: [ @@ -1814,7 +1568,7 @@ fn iris() -> ColorScaleSet { .unwrap() } -fn indigo() -> ColorScaleSet { +pub(crate) fn indigo() -> ColorScaleSet { StaticColorScaleSet { scale: "Indigo", light: [ @@ -1878,7 +1632,7 @@ fn indigo() -> ColorScaleSet { .unwrap() } -fn blue() -> ColorScaleSet { +pub(crate) fn blue() -> ColorScaleSet { StaticColorScaleSet { scale: "Blue", light: [ @@ -1942,7 +1696,7 @@ fn blue() -> ColorScaleSet { .unwrap() } -fn cyan() -> ColorScaleSet { +pub(crate) fn cyan() -> ColorScaleSet { StaticColorScaleSet { scale: "Cyan", light: [ @@ -2006,7 +1760,7 @@ fn cyan() -> ColorScaleSet { .unwrap() } -fn teal() -> ColorScaleSet { +pub(crate) fn teal() -> ColorScaleSet { StaticColorScaleSet { scale: "Teal", light: [ @@ -2070,7 +1824,7 @@ fn teal() -> ColorScaleSet { .unwrap() } -fn jade() -> ColorScaleSet { +pub(crate) fn jade() -> ColorScaleSet { StaticColorScaleSet { scale: "Jade", light: [ @@ -2134,7 +1888,7 @@ fn jade() -> ColorScaleSet { .unwrap() } -fn green() -> ColorScaleSet { +pub(crate) fn green() -> ColorScaleSet { StaticColorScaleSet { scale: "Green", light: [ @@ -2198,7 +1952,7 @@ fn green() -> ColorScaleSet { .unwrap() } -fn grass() -> ColorScaleSet { +pub(crate) fn grass() -> ColorScaleSet { StaticColorScaleSet { scale: "Grass", light: [ @@ -2262,7 +2016,7 @@ fn grass() -> ColorScaleSet { .unwrap() } -fn lime() -> ColorScaleSet { +pub(crate) fn lime() -> ColorScaleSet { StaticColorScaleSet { scale: "Lime", light: [ @@ -2326,7 +2080,7 @@ fn lime() -> ColorScaleSet { .unwrap() } -fn mint() -> ColorScaleSet { +pub(crate) fn mint() -> ColorScaleSet { StaticColorScaleSet { scale: "Mint", light: [ @@ -2390,7 +2144,7 @@ fn mint() -> ColorScaleSet { .unwrap() } -fn sky() -> ColorScaleSet { +pub(crate) fn sky() -> ColorScaleSet { StaticColorScaleSet { scale: "Sky", light: [ @@ -2454,7 +2208,7 @@ fn sky() -> ColorScaleSet { .unwrap() } -fn black() -> ColorScaleSet { +pub(crate) fn black() -> ColorScaleSet { StaticColorScaleSet { scale: "Black", light: [ @@ -2518,7 +2272,7 @@ fn black() -> ColorScaleSet { .unwrap() } -fn white() -> ColorScaleSet { +pub(crate) fn white() -> ColorScaleSet { StaticColorScaleSet { scale: "White", light: [ diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 40fb7df7cfd85d1ed33edcd16ad562af4519ee5d..8502f433f4a919d7d661f00e55d0dd353ff46fc5 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,58 +1,56 @@ -use std::sync::Arc; - use crate::{ - colors::{StatusColors, SystemColors, ThemeColors, ThemeStyles}, - default_color_scales, Appearance, PlayerColors, SyntaxTheme, Theme, ThemeFamily, + one_themes::{one_dark, one_family}, + Theme, ThemeFamily, }; -fn zed_pro_daylight() -> Theme { - Theme { - id: "zed_pro_daylight".to_string(), - name: "Zed Pro Daylight".into(), - appearance: Appearance::Light, - styles: ThemeStyles { - system: SystemColors::default(), - colors: ThemeColors::default_light(), - status: StatusColors::default(), - player: PlayerColors::default_light(), - syntax: Arc::new(SyntaxTheme::default_light()), - }, - } -} +// fn zed_pro_daylight() -> Theme { +// Theme { +// id: "zed_pro_daylight".to_string(), +// name: "Zed Pro Daylight".into(), +// appearance: Appearance::Light, +// styles: ThemeStyles { +// system: SystemColors::default(), +// colors: ThemeColors::light(), +// status: StatusColors::light(), +// player: PlayerColors::light(), +// syntax: Arc::new(SyntaxTheme::light()), +// }, +// } +// } -pub(crate) fn zed_pro_moonlight() -> Theme { - Theme { - id: "zed_pro_moonlight".to_string(), - name: "Zed Pro Moonlight".into(), - appearance: Appearance::Dark, - styles: ThemeStyles { - system: SystemColors::default(), - colors: ThemeColors::default_dark(), - status: StatusColors::default(), - player: PlayerColors::default(), - syntax: Arc::new(SyntaxTheme::default_dark()), - }, - } -} +// pub(crate) fn zed_pro_moonlight() -> Theme { +// Theme { +// id: "zed_pro_moonlight".to_string(), +// name: "Zed Pro Moonlight".into(), +// appearance: Appearance::Dark, +// styles: ThemeStyles { +// system: SystemColors::default(), +// colors: ThemeColors::dark(), +// status: StatusColors::dark(), +// player: PlayerColors::dark(), +// syntax: Arc::new(SyntaxTheme::dark()), +// }, +// } +// } -pub fn zed_pro_family() -> ThemeFamily { - ThemeFamily { - id: "zed_pro".to_string(), - name: "Zed Pro".into(), - author: "Zed Team".into(), - themes: vec![zed_pro_daylight(), zed_pro_moonlight()], - scales: default_color_scales(), - } -} +// pub fn zed_pro_family() -> ThemeFamily { +// ThemeFamily { +// id: "zed_pro".to_string(), +// name: "Zed Pro".into(), +// author: "Zed Team".into(), +// themes: vec![zed_pro_daylight(), zed_pro_moonlight()], +// scales: default_color_scales(), +// } +// } impl Default for ThemeFamily { fn default() -> Self { - zed_pro_family() + one_family() } } impl Default for Theme { fn default() -> Self { - zed_pro_daylight() + one_dark() } } diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs new file mode 100644 index 0000000000000000000000000000000000000000..6e32eace7350414b002dfda1d3bf242665401e74 --- /dev/null +++ b/crates/theme2/src/one_themes.rs @@ -0,0 +1,198 @@ +use std::sync::Arc; + +use gpui::{hsla, FontStyle, FontWeight, HighlightStyle}; + +use crate::{ + default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, + ThemeColors, ThemeFamily, ThemeStyles, +}; + +pub fn one_family() -> ThemeFamily { + ThemeFamily { + id: "one".to_string(), + name: "One".into(), + author: "".into(), + themes: vec![one_dark()], + scales: default_color_scales(), + } +} + +pub(crate) fn one_dark() -> Theme { + let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.); + let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.); + let elevated_surface = hsla(220. / 360., 12. / 100., 18. / 100., 1.); + + let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0); + let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0); + let green = hsla(95. / 360., 38. / 100., 62. / 100., 1.0); + let orange = hsla(29. / 360., 54. / 100., 61. / 100., 1.0); + let purple = hsla(286. / 360., 51. / 100., 64. / 100., 1.0); + let red = hsla(355. / 360., 65. / 100., 65. / 100., 1.0); + let teal = hsla(187. / 360., 47. / 100., 55. / 100., 1.0); + let yellow = hsla(39. / 360., 67. / 100., 69. / 100., 1.0); + + Theme { + id: "one_dark".to_string(), + name: "One Dark".into(), + appearance: Appearance::Dark, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors { + border: hsla(225. / 360., 13. / 100., 12. / 100., 1.), + border_variant: hsla(228. / 360., 8. / 100., 25. / 100., 1.), + border_focused: hsla(223. / 360., 78. / 100., 65. / 100., 1.), + border_selected: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), + border_transparent: SystemColors::default().transparent, + border_disabled: hsla(222.0 / 360., 11.6 / 100., 33.7 / 100., 1.0), + elevated_surface_background: elevated_surface, + surface_background: bg, + background: bg, + element_background: elevated_surface, + element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), + element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), + element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0), + ghost_element_background: SystemColors::default().transparent, + ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), + ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), + ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + ghost_element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + text: hsla(222.9 / 360., 9.1 / 100., 84.9 / 100., 1.0), + text_muted: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), + text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), + text_disabled: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), + text_accent: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), + icon: hsla(222.9 / 360., 9.9 / 100., 86.1 / 100., 1.0), + icon_muted: hsla(220.0 / 360., 12.1 / 100., 66.1 / 100., 1.0), + icon_disabled: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), + icon_placeholder: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), + icon_accent: blue.into(), + status_bar_background: bg, + title_bar_background: bg, + toolbar_background: editor, + tab_bar_background: bg, + tab_inactive_background: bg, + tab_active_background: editor, + editor_background: editor, + editor_gutter_background: editor, + editor_subheader_background: bg, + editor_active_line_background: hsla(222.9 / 360., 13.5 / 100., 20.4 / 100., 1.0), + editor_highlighted_line_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.1), + editor_line_number: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), + editor_active_line_number: hsla(216.0 / 360., 5.9 / 100., 49.6 / 100., 1.0), + editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), + editor_wrap_guide: gpui::black(), + editor_active_wrap_guide: gpui::red(), + editor_document_highlight_read_background: hsla( + 207.8 / 360., + 81. / 100., + 66. / 100., + 0.2, + ), + editor_document_highlight_write_background: gpui::red(), + terminal_background: bg, + // todo!("Use one colors for terminal") + terminal_ansi_black: crate::black().dark().step_12(), + terminal_ansi_red: crate::red().dark().step_11(), + terminal_ansi_green: crate::green().dark().step_11(), + terminal_ansi_yellow: crate::yellow().dark().step_11(), + terminal_ansi_blue: crate::blue().dark().step_11(), + terminal_ansi_magenta: crate::violet().dark().step_11(), + terminal_ansi_cyan: crate::cyan().dark().step_11(), + terminal_ansi_white: crate::neutral().dark().step_12(), + terminal_ansi_bright_black: crate::black().dark().step_11(), + terminal_ansi_bright_red: crate::red().dark().step_10(), + terminal_ansi_bright_green: crate::green().dark().step_10(), + terminal_ansi_bright_yellow: crate::yellow().dark().step_10(), + terminal_ansi_bright_blue: crate::blue().dark().step_10(), + terminal_ansi_bright_magenta: crate::violet().dark().step_10(), + terminal_ansi_bright_cyan: crate::cyan().dark().step_10(), + terminal_ansi_bright_white: crate::neutral().dark().step_11(), + }, + status: StatusColors { + conflict: yellow, + created: green, + deleted: red, + error: red, + hidden: gray, + hint: blue, + ignored: gray, + info: blue, + modified: yellow, + predictive: gray, + renamed: blue, + success: green, + unreachable: gray, + warning: yellow, + }, + player: PlayerColors::dark(), + syntax: Arc::new(SyntaxTheme { + highlights: vec![ + ("attribute".into(), purple.into()), + ("boolean".into(), orange.into()), + ("comment".into(), gray.into()), + ("comment.doc".into(), gray.into()), + ("constant".into(), yellow.into()), + ("constructor".into(), blue.into()), + ("embedded".into(), HighlightStyle::default()), + ( + "emphasis".into(), + HighlightStyle { + font_style: Some(FontStyle::Italic), + ..HighlightStyle::default() + }, + ), + ( + "emphasis.strong".into(), + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..HighlightStyle::default() + }, + ), + ("enum".into(), HighlightStyle::default()), + ("function".into(), blue.into()), + ("function.method".into(), blue.into()), + ("function.definition".into(), blue.into()), + ("hint".into(), blue.into()), + ("keyword".into(), purple.into()), + ("label".into(), HighlightStyle::default()), + ("link_text".into(), blue.into()), + ( + "link_uri".into(), + HighlightStyle { + color: Some(teal.into()), + font_style: Some(FontStyle::Italic), + ..HighlightStyle::default() + }, + ), + ("number".into(), orange.into()), + ("operator".into(), HighlightStyle::default()), + ("predictive".into(), HighlightStyle::default()), + ("preproc".into(), HighlightStyle::default()), + ("primary".into(), HighlightStyle::default()), + ("property".into(), red.into()), + ("punctuation".into(), HighlightStyle::default()), + ("punctuation.bracket".into(), HighlightStyle::default()), + ("punctuation.delimiter".into(), HighlightStyle::default()), + ("punctuation.list_marker".into(), HighlightStyle::default()), + ("punctuation.special".into(), HighlightStyle::default()), + ("string".into(), green.into()), + ("string.escape".into(), HighlightStyle::default()), + ("string.regex".into(), red.into()), + ("string.special".into(), HighlightStyle::default()), + ("string.special.symbol".into(), HighlightStyle::default()), + ("tag".into(), HighlightStyle::default()), + ("text.literal".into(), HighlightStyle::default()), + ("title".into(), HighlightStyle::default()), + ("type".into(), teal.into()), + ("variable".into(), HighlightStyle::default()), + ("variable.special".into(), red.into()), + ("variant".into(), HighlightStyle::default()), + ], + inlay_style: HighlightStyle::default(), + suggestion_style: HighlightStyle::default(), + }), + }, + } +} diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 0c61f6f22468901e6c7c78e8664d075c5c82779c..19af0ede51c1ce4467ac57d91dd8ffd86c8eb93c 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -6,8 +6,8 @@ use gpui::{HighlightStyle, SharedString}; use refineable::Refineable; use crate::{ - zed_pro_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, - ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, + one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, + Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, }; pub struct ThemeRegistry { @@ -38,17 +38,17 @@ impl ThemeRegistry { fn insert_user_themes(&mut self, themes: impl IntoIterator) { self.insert_themes(themes.into_iter().map(|user_theme| { let mut theme_colors = match user_theme.appearance { - Appearance::Light => ThemeColors::default_light(), - Appearance::Dark => ThemeColors::default_dark(), + Appearance::Light => ThemeColors::light(), + Appearance::Dark => ThemeColors::dark(), }; theme_colors.refine(&user_theme.styles.colors); - let mut status_colors = StatusColors::default(); + let mut status_colors = StatusColors::dark(); status_colors.refine(&user_theme.styles.status); let mut syntax_colors = match user_theme.appearance { - Appearance::Light => SyntaxTheme::default_light(), - Appearance::Dark => SyntaxTheme::default_dark(), + Appearance::Light => SyntaxTheme::light(), + Appearance::Dark => SyntaxTheme::dark(), }; if let Some(user_syntax) = user_theme.styles.syntax { syntax_colors.highlights = user_syntax @@ -76,7 +76,10 @@ impl ThemeRegistry { system: SystemColors::default(), colors: theme_colors, status: status_colors, - player: PlayerColors::default(), + player: match user_theme.appearance { + Appearance::Light => PlayerColors::light(), + Appearance::Dark => PlayerColors::dark(), + }, syntax: Arc::new(syntax_colors), }, } @@ -105,7 +108,7 @@ impl Default for ThemeRegistry { themes: HashMap::default(), }; - this.insert_theme_families([zed_pro_family()]); + this.insert_theme_families([one_family()]); #[cfg(not(feature = "importing-themes"))] this.insert_user_theme_familes(crate::all_user_themes()); diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index c57576840119e42e42e3f305cd0dd4fc5463cf81..8a15b52641bc30ca03e4d32c65070b89d5b8e79c 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -1,3 +1,4 @@ +use crate::one_themes::one_dark; use crate::{Theme, ThemeRegistry}; use anyhow::Result; use gpui::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels}; @@ -129,7 +130,7 @@ impl settings::Settings for ThemeSettings { buffer_line_height: defaults.buffer_line_height.unwrap(), active_theme: themes .get(defaults.theme.as_ref().unwrap()) - .or(themes.get("Zed Pro Moonlight")) + .or(themes.get(&one_dark().name)) .unwrap(), }; diff --git a/crates/theme2/src/styles.rs b/crates/theme2/src/styles.rs new file mode 100644 index 0000000000000000000000000000000000000000..18f9e76581fb3210da9174378f4fd2e314d19a3b --- /dev/null +++ b/crates/theme2/src/styles.rs @@ -0,0 +1,11 @@ +mod colors; +mod players; +mod status; +mod syntax; +mod system; + +pub use colors::*; +pub use players::*; +pub use status::*; +pub use syntax::*; +pub use system::*; diff --git a/crates/theme2/src/colors.rs b/crates/theme2/src/styles/colors.rs similarity index 94% rename from crates/theme2/src/colors.rs rename to crates/theme2/src/styles/colors.rs index b8cceebea820f9a7fe61d39bef2d32784006bbc4..1d4917ac00b99797c077623e13f2ac97bde87dd8 100644 --- a/crates/theme2/src/colors.rs +++ b/crates/theme2/src/styles/colors.rs @@ -1,31 +1,8 @@ -use crate::{PlayerColors, SyntaxTheme}; use gpui::Hsla; use refineable::Refineable; use std::sync::Arc; -#[derive(Clone)] -pub struct SystemColors { - pub transparent: Hsla, - pub mac_os_traffic_light_red: Hsla, - pub mac_os_traffic_light_yellow: Hsla, - pub mac_os_traffic_light_green: Hsla, -} - -#[derive(Refineable, Clone, Debug)] -#[refineable(Debug, serde::Deserialize)] -pub struct StatusColors { - pub conflict: Hsla, - pub created: Hsla, - pub deleted: Hsla, - pub error: Hsla, - pub hidden: Hsla, - pub ignored: Hsla, - pub info: Hsla, - pub modified: Hsla, - pub renamed: Hsla, - pub success: Hsla, - pub warning: Hsla, -} +use crate::{PlayerColors, StatusColors, SyntaxTheme, SystemColors}; #[derive(Refineable, Clone, Debug)] #[refineable(Debug, serde::Deserialize)] @@ -259,7 +236,7 @@ mod tests { #[test] fn override_a_single_theme_color() { - let mut colors = ThemeColors::default_light(); + let mut colors = ThemeColors::light(); let magenta: Hsla = gpui::rgb(0xff00ff); @@ -277,7 +254,7 @@ mod tests { #[test] fn override_multiple_theme_colors() { - let mut colors = ThemeColors::default_light(); + let mut colors = ThemeColors::light(); let magenta: Hsla = gpui::rgb(0xff00ff); let green: Hsla = gpui::rgb(0x00ff00); diff --git a/crates/theme2/src/players.rs b/crates/theme2/src/styles/players.rs similarity index 66% rename from crates/theme2/src/players.rs rename to crates/theme2/src/styles/players.rs index 28489285a7602e99ff21c847269b9f8669aefc5a..dfb0a6ff4eb448cf123e99c346cfe107a4b78318 100644 --- a/crates/theme2/src/players.rs +++ b/crates/theme2/src/styles/players.rs @@ -16,6 +16,107 @@ pub struct PlayerColor { #[derive(Clone)] pub struct PlayerColors(pub Vec); +impl Default for PlayerColors { + /// Don't use this! + /// We have to have a default to be `[refineable::Refinable]`. + /// todo!("Find a way to not need this for Refinable") + fn default() -> Self { + Self::dark() + } +} + +impl PlayerColors { + pub fn dark() -> Self { + Self(vec![ + PlayerColor { + cursor: blue().dark().step_9(), + background: blue().dark().step_5(), + selection: blue().dark().step_3(), + }, + PlayerColor { + cursor: orange().dark().step_9(), + background: orange().dark().step_5(), + selection: orange().dark().step_3(), + }, + PlayerColor { + cursor: pink().dark().step_9(), + background: pink().dark().step_5(), + selection: pink().dark().step_3(), + }, + PlayerColor { + cursor: lime().dark().step_9(), + background: lime().dark().step_5(), + selection: lime().dark().step_3(), + }, + PlayerColor { + cursor: purple().dark().step_9(), + background: purple().dark().step_5(), + selection: purple().dark().step_3(), + }, + PlayerColor { + cursor: amber().dark().step_9(), + background: amber().dark().step_5(), + selection: amber().dark().step_3(), + }, + PlayerColor { + cursor: jade().dark().step_9(), + background: jade().dark().step_5(), + selection: jade().dark().step_3(), + }, + PlayerColor { + cursor: red().dark().step_9(), + background: red().dark().step_5(), + selection: red().dark().step_3(), + }, + ]) + } + + pub fn light() -> Self { + Self(vec![ + PlayerColor { + cursor: blue().light().step_9(), + background: blue().light().step_4(), + selection: blue().light().step_3(), + }, + PlayerColor { + cursor: orange().light().step_9(), + background: orange().light().step_4(), + selection: orange().light().step_3(), + }, + PlayerColor { + cursor: pink().light().step_9(), + background: pink().light().step_4(), + selection: pink().light().step_3(), + }, + PlayerColor { + cursor: lime().light().step_9(), + background: lime().light().step_4(), + selection: lime().light().step_3(), + }, + PlayerColor { + cursor: purple().light().step_9(), + background: purple().light().step_4(), + selection: purple().light().step_3(), + }, + PlayerColor { + cursor: amber().light().step_9(), + background: amber().light().step_4(), + selection: amber().light().step_3(), + }, + PlayerColor { + cursor: jade().light().step_9(), + background: jade().light().step_4(), + selection: jade().light().step_3(), + }, + PlayerColor { + cursor: red().light().step_9(), + background: red().light().step_4(), + selection: red().light().step_3(), + }, + ]) + } +} + impl PlayerColors { pub fn local(&self) -> PlayerColor { // todo!("use a valid color"); @@ -36,6 +137,8 @@ impl PlayerColors { #[cfg(feature = "stories")] pub use stories::*; +use crate::{amber, blue, jade, lime, orange, pink, purple, red}; + #[cfg(feature = "stories")] mod stories { use super::*; diff --git a/crates/theme2/src/styles/status.rs b/crates/theme2/src/styles/status.rs new file mode 100644 index 0000000000000000000000000000000000000000..db0f4758250b613d2c544f6456bae5c302cba283 --- /dev/null +++ b/crates/theme2/src/styles/status.rs @@ -0,0 +1,134 @@ +use gpui::Hsla; +use refineable::Refineable; + +use crate::{blue, grass, neutral, red, yellow}; + +#[derive(Refineable, Clone, Debug)] +#[refineable(Debug, serde::Deserialize)] +pub struct StatusColors { + /// Indicates some kind of conflict, like a file changed on disk while it was open, or + /// merge conflicts in a Git repository. + pub conflict: Hsla, + + /// Indicates something new, like a new file added to a Git repository. + pub created: Hsla, + + /// Indicates that something no longer exists, like a deleted file. + pub deleted: Hsla, + + /// Indicates a system error, a failed operation or a diagnostic error. + pub error: Hsla, + + /// Represents a hidden status, such as a file being hidden in a file tree. + pub hidden: Hsla, + + /// Indicates a hint or some kind of additional information. + pub hint: Hsla, + + /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. + pub ignored: Hsla, + + /// Represents informational status updates or messages. + pub info: Hsla, + + /// Indicates a changed or altered status, like a file that has been edited. + pub modified: Hsla, + + /// Indicates something that is predicted, like automatic code completion, or generated code. + pub predictive: Hsla, + + /// Represents a renamed status, such as a file that has been renamed. + pub renamed: Hsla, + + /// Indicates a successful operation or task completion. + pub success: Hsla, + + /// Indicates some kind of unreachable status, like a block of code that can never be reached. + pub unreachable: Hsla, + + /// Represents a warning status, like an operation that is about to fail. + pub warning: Hsla, +} + +impl Default for StatusColors { + /// Don't use this! + /// We have to have a default to be `[refineable::Refinable]`. + /// todo!("Find a way to not need this for Refinable") + fn default() -> Self { + Self::dark() + } +} + +pub struct DiagnosticColors { + pub error: Hsla, + pub warning: Hsla, + pub info: Hsla, +} + +pub struct GitStatusColors { + pub created: Hsla, + pub deleted: Hsla, + pub modified: Hsla, + pub renamed: Hsla, + pub conflict: Hsla, + pub ignored: Hsla, +} + +impl StatusColors { + pub fn dark() -> Self { + Self { + conflict: red().dark().step_9(), + created: grass().dark().step_9(), + deleted: red().dark().step_9(), + error: red().dark().step_9(), + hidden: neutral().dark().step_9(), + hint: blue().dark().step_9(), + ignored: neutral().dark().step_9(), + info: blue().dark().step_9(), + modified: yellow().dark().step_9(), + predictive: neutral().dark_alpha().step_9(), + renamed: blue().dark().step_9(), + success: grass().dark().step_9(), + unreachable: neutral().dark().step_10(), + warning: yellow().dark().step_9(), + } + } + + pub fn light() -> Self { + Self { + conflict: red().light().step_9(), + created: grass().light().step_9(), + deleted: red().light().step_9(), + error: red().light().step_9(), + hidden: neutral().light().step_9(), + hint: blue().light().step_9(), + ignored: neutral().light().step_9(), + info: blue().light().step_9(), + modified: yellow().light().step_9(), + predictive: neutral().light_alpha().step_9(), + renamed: blue().light().step_9(), + success: grass().light().step_9(), + unreachable: neutral().light().step_10(), + warning: yellow().light().step_9(), + } + } + + pub fn diagnostic(&self) -> DiagnosticColors { + DiagnosticColors { + error: self.error, + warning: self.warning, + info: self.info, + } + } + + pub fn git(&self) -> GitStatusColors { + GitStatusColors { + created: self.created, + deleted: self.deleted, + modified: self.modified, + renamed: self.renamed, + conflict: self.conflict, + ignored: self.ignored, + } + } +} diff --git a/crates/theme2/src/styles/syntax.rs b/crates/theme2/src/styles/syntax.rs new file mode 100644 index 0000000000000000000000000000000000000000..8675d30e3a00a94d3ea05efa018dfd7775dabace --- /dev/null +++ b/crates/theme2/src/styles/syntax.rs @@ -0,0 +1,170 @@ +use gpui::{HighlightStyle, Hsla}; + +use crate::{ + blue, cyan, gold, indigo, iris, jade, lime, mint, neutral, orange, plum, purple, red, sky, + tomato, yellow, +}; + +#[derive(Clone, Default)] +pub struct SyntaxTheme { + pub highlights: Vec<(String, HighlightStyle)>, + // todo!("Remove this in favor of StatusColor.hint") + // If this should be overridable we should move it to ThemeColors + pub inlay_style: HighlightStyle, + // todo!("Remove this in favor of StatusColor.prediction") + // If this should be overridable we should move it to ThemeColors + pub suggestion_style: HighlightStyle, +} + +impl SyntaxTheme { + pub fn light() -> Self { + Self { + highlights: vec![ + ("attribute".into(), cyan().light().step_11().into()), + ("boolean".into(), tomato().light().step_11().into()), + ("comment".into(), neutral().light().step_11().into()), + ("comment.doc".into(), iris().light().step_12().into()), + ("constant".into(), red().light().step_9().into()), + ("constructor".into(), red().light().step_9().into()), + ("embedded".into(), red().light().step_9().into()), + ("emphasis".into(), red().light().step_9().into()), + ("emphasis.strong".into(), red().light().step_9().into()), + ("enum".into(), red().light().step_9().into()), + ("function".into(), red().light().step_9().into()), + ("hint".into(), red().light().step_9().into()), + ("keyword".into(), orange().light().step_11().into()), + ("label".into(), red().light().step_9().into()), + ("link_text".into(), red().light().step_9().into()), + ("link_uri".into(), red().light().step_9().into()), + ("number".into(), red().light().step_9().into()), + ("operator".into(), red().light().step_9().into()), + ("predictive".into(), red().light().step_9().into()), + ("preproc".into(), red().light().step_9().into()), + ("primary".into(), red().light().step_9().into()), + ("property".into(), red().light().step_9().into()), + ("punctuation".into(), neutral().light().step_11().into()), + ( + "punctuation.bracket".into(), + neutral().light().step_11().into(), + ), + ( + "punctuation.delimiter".into(), + neutral().light().step_11().into(), + ), + ( + "punctuation.list_marker".into(), + blue().light().step_11().into(), + ), + ("punctuation.special".into(), red().light().step_9().into()), + ("string".into(), jade().light().step_11().into()), + ("string.escape".into(), red().light().step_9().into()), + ("string.regex".into(), tomato().light().step_11().into()), + ("string.special".into(), red().light().step_9().into()), + ( + "string.special.symbol".into(), + red().light().step_9().into(), + ), + ("tag".into(), red().light().step_9().into()), + ("text.literal".into(), red().light().step_9().into()), + ("title".into(), red().light().step_9().into()), + ("type".into(), red().light().step_9().into()), + ("variable".into(), red().light().step_9().into()), + ("variable.special".into(), red().light().step_9().into()), + ("variant".into(), red().light().step_9().into()), + ], + inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style") + suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style") + } + } + + pub fn dark() -> Self { + Self { + highlights: vec![ + ("attribute".into(), tomato().dark().step_11().into()), + ("boolean".into(), tomato().dark().step_11().into()), + ("comment".into(), neutral().dark().step_11().into()), + ("comment.doc".into(), iris().dark().step_12().into()), + ("constant".into(), orange().dark().step_11().into()), + ("constructor".into(), gold().dark().step_11().into()), + ("embedded".into(), red().dark().step_11().into()), + ("emphasis".into(), red().dark().step_11().into()), + ("emphasis.strong".into(), red().dark().step_11().into()), + ("enum".into(), yellow().dark().step_11().into()), + ("function".into(), blue().dark().step_11().into()), + ("hint".into(), indigo().dark().step_11().into()), + ("keyword".into(), plum().dark().step_11().into()), + ("label".into(), red().dark().step_11().into()), + ("link_text".into(), red().dark().step_11().into()), + ("link_uri".into(), red().dark().step_11().into()), + ("number".into(), red().dark().step_11().into()), + ("operator".into(), red().dark().step_11().into()), + ("predictive".into(), red().dark().step_11().into()), + ("preproc".into(), red().dark().step_11().into()), + ("primary".into(), red().dark().step_11().into()), + ("property".into(), red().dark().step_11().into()), + ("punctuation".into(), neutral().dark().step_11().into()), + ( + "punctuation.bracket".into(), + neutral().dark().step_11().into(), + ), + ( + "punctuation.delimiter".into(), + neutral().dark().step_11().into(), + ), + ( + "punctuation.list_marker".into(), + blue().dark().step_11().into(), + ), + ("punctuation.special".into(), red().dark().step_11().into()), + ("string".into(), lime().dark().step_11().into()), + ("string.escape".into(), orange().dark().step_11().into()), + ("string.regex".into(), tomato().dark().step_11().into()), + ("string.special".into(), red().dark().step_11().into()), + ( + "string.special.symbol".into(), + red().dark().step_11().into(), + ), + ("tag".into(), red().dark().step_11().into()), + ("text.literal".into(), purple().dark().step_11().into()), + ("title".into(), sky().dark().step_11().into()), + ("type".into(), mint().dark().step_11().into()), + ("variable".into(), red().dark().step_11().into()), + ("variable.special".into(), red().dark().step_11().into()), + ("variant".into(), red().dark().step_11().into()), + ], + inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style") + suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style") + } + } + + // TOOD: Get this working with `#[cfg(test)]`. Why isn't it? + pub fn new_test(colors: impl IntoIterator) -> Self { + SyntaxTheme { + highlights: colors + .into_iter() + .map(|(key, color)| { + ( + key.to_owned(), + HighlightStyle { + color: Some(color), + ..Default::default() + }, + ) + }) + .collect(), + inlay_style: HighlightStyle::default(), + suggestion_style: HighlightStyle::default(), + } + } + + pub fn get(&self, name: &str) -> HighlightStyle { + self.highlights + .iter() + .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None }) + .unwrap_or_default() + } + + pub fn color(&self, name: &str) -> Hsla { + self.get(name).color.unwrap_or_default() + } +} diff --git a/crates/theme2/src/styles/system.rs b/crates/theme2/src/styles/system.rs new file mode 100644 index 0000000000000000000000000000000000000000..aeb0865155d68aa8e167421b1ce79895120203ff --- /dev/null +++ b/crates/theme2/src/styles/system.rs @@ -0,0 +1,20 @@ +use gpui::{hsla, Hsla}; + +#[derive(Clone)] +pub struct SystemColors { + pub transparent: Hsla, + pub mac_os_traffic_light_red: Hsla, + pub mac_os_traffic_light_yellow: Hsla, + pub mac_os_traffic_light_green: Hsla, +} + +impl Default for SystemColors { + fn default() -> Self { + Self { + transparent: hsla(0.0, 0.0, 0.0, 0.0), + mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0), + mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0), + mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0), + } + } +} diff --git a/crates/theme2/src/syntax.rs b/crates/theme2/src/syntax.rs deleted file mode 100644 index 8aac238555f1190272a8f959da142901fb70d326..0000000000000000000000000000000000000000 --- a/crates/theme2/src/syntax.rs +++ /dev/null @@ -1,41 +0,0 @@ -use gpui::{HighlightStyle, Hsla}; - -#[derive(Clone, Default)] -pub struct SyntaxTheme { - pub highlights: Vec<(String, HighlightStyle)>, - pub inlay_style: HighlightStyle, - pub suggestion_style: HighlightStyle, -} - -impl SyntaxTheme { - // TOOD: Get this working with `#[cfg(test)]`. Why isn't it? - pub fn new_test(colors: impl IntoIterator) -> Self { - SyntaxTheme { - highlights: colors - .into_iter() - .map(|(key, color)| { - ( - key.to_owned(), - HighlightStyle { - color: Some(color), - ..Default::default() - }, - ) - }) - .collect(), - inlay_style: HighlightStyle::default(), - suggestion_style: HighlightStyle::default(), - } - } - - pub fn get(&self, name: &str) -> HighlightStyle { - self.highlights - .iter() - .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None }) - .unwrap_or_default() - } - - pub fn color(&self, name: &str) -> Hsla { - self.get(name).color.unwrap_or_default() - } -} diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index 7e2085de4e7249ccb1925f1f0c23555f81dd08fc..b6790b5a6ff534cb8080e7ae88a28c72cbeeefb7 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -1,11 +1,10 @@ -mod colors; mod default_colors; mod default_theme; -mod players; +mod one_themes; mod registry; mod scale; mod settings; -mod syntax; +mod styles; #[cfg(not(feature = "importing-themes"))] mod themes; mod user_theme; @@ -13,14 +12,12 @@ mod user_theme; use std::sync::Arc; use ::settings::Settings; -pub use colors::*; pub use default_colors::*; pub use default_theme::*; -pub use players::*; pub use registry::*; pub use scale::*; pub use settings::*; -pub use syntax::*; +pub use styles::*; #[cfg(not(feature = "importing-themes"))] pub use themes::*; pub use user_theme::*; diff --git a/crates/theme2/util/hex_to_hsla.py b/crates/theme2/util/hex_to_hsla.py new file mode 100644 index 0000000000000000000000000000000000000000..17faa186d8c6e6ae78ea907804a1a7b79f78bcfc --- /dev/null +++ b/crates/theme2/util/hex_to_hsla.py @@ -0,0 +1,35 @@ +import colorsys +import sys + +def hex_to_rgb(hex): + hex = hex.lstrip('#') + if len(hex) == 8: # 8 digit hex color + r, g, b, a = (int(hex[i:i+2], 16) for i in (0, 2, 4, 6)) + return r, g, b, a / 255.0 + else: # 6 digit hex color + return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4)) + (1.0,) + +def rgb_to_hsla(rgb): + h, l, s = colorsys.rgb_to_hls(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0) + a = rgb[3] # alpha value + return (round(h * 360, 1), round(s * 100, 1), round(l * 100, 1), round(a, 3)) + +def hex_to_hsla(hex): + return rgb_to_hsla(hex_to_rgb(hex)) + +if len(sys.argv) != 2: + print("Usage: python util/hex_to_hsla.py <6 or 8 digit hex color or comma-separated list of colors>") +else: + input_arg = sys.argv[1] + if ',' in input_arg: # comma-separated list of colors + hex_colors = input_arg.split(',') + hslas = [] # output array + for hex_color in hex_colors: + hex_color = hex_color.strip("'\" ") + h, s, l, a = hex_to_hsla(hex_color) + hslas.append(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})") + print(hslas) + else: # single color + hex_color = input_arg.strip("'\"") + h, s, l, a = hex_to_hsla(hex_color) + print(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})") diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index bd02e694edd45561373f39e2673d7e290dcf30cb..a3e5a870a6b61cad638a1a8b174c364273d559f2 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -1,50 +1,43 @@ -use std::collections::HashSet; - -use strum::{EnumIter, IntoEnumIterator}; +use gpui::Action; +use strum::EnumIter; use crate::prelude::*; #[derive(Component)] -pub struct Keybinding { +pub struct KeyBinding { /// A keybinding consists of a key and a set of modifier keys. /// More then one keybinding produces a chord. /// /// This should always contain at least one element. - keybinding: Vec<(String, ModifierKeys)>, + key_binding: gpui::KeyBinding, } -impl Keybinding { - pub fn new(key: String, modifiers: ModifierKeys) -> Self { - Self { - keybinding: vec![(key, modifiers)], - } +impl KeyBinding { + pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option { + // todo! this last is arbitrary, we want to prefer users key bindings over defaults, + // and vim over normal (in vim mode), etc. + let key_binding = cx.bindings_for_action(action).last().cloned()?; + Some(Self::new(key_binding)) } - pub fn new_chord( - first_note: (String, ModifierKeys), - second_note: (String, ModifierKeys), - ) -> Self { - Self { - keybinding: vec![first_note, second_note], - } + pub fn new(key_binding: gpui::KeyBinding) -> Self { + Self { key_binding } } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { div() .flex() .gap_2() - .children(self.keybinding.iter().map(|(key, modifiers)| { + .children(self.key_binding.keystrokes().iter().map(|keystroke| { div() .flex() .gap_1() - .children(ModifierKey::iter().filter_map(|modifier| { - if modifiers.0.contains(&modifier) { - Some(Key::new(modifier.glyph().to_string())) - } else { - None - } - })) - .child(Key::new(key.clone())) + .when(keystroke.modifiers.function, |el| el.child(Key::new("fn"))) + .when(keystroke.modifiers.control, |el| el.child(Key::new("^"))) + .when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥"))) + .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘"))) + .when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧"))) + .child(Key::new(keystroke.key.clone())) })) } } @@ -81,76 +74,6 @@ pub enum ModifierKey { Shift, } -impl ModifierKey { - /// Returns the glyph for the [`ModifierKey`]. - pub fn glyph(&self) -> char { - match self { - Self::Control => '^', - Self::Alt => '⌥', - Self::Command => '⌘', - Self::Shift => '⇧', - } - } -} - -#[derive(Clone)] -pub struct ModifierKeys(HashSet); - -impl ModifierKeys { - pub fn new() -> Self { - Self(HashSet::new()) - } - - pub fn all() -> Self { - Self(HashSet::from_iter(ModifierKey::iter())) - } - - pub fn add(mut self, modifier: ModifierKey) -> Self { - self.0.insert(modifier); - self - } - - pub fn control(mut self, control: bool) -> Self { - if control { - self.0.insert(ModifierKey::Control); - } else { - self.0.remove(&ModifierKey::Control); - } - - self - } - - pub fn alt(mut self, alt: bool) -> Self { - if alt { - self.0.insert(ModifierKey::Alt); - } else { - self.0.remove(&ModifierKey::Alt); - } - - self - } - - pub fn command(mut self, command: bool) -> Self { - if command { - self.0.insert(ModifierKey::Command); - } else { - self.0.remove(&ModifierKey::Command); - } - - self - } - - pub fn shift(mut self, shift: bool) -> Self { - if shift { - self.0.insert(ModifierKey::Shift); - } else { - self.0.remove(&ModifierKey::Shift); - } - - self - } -} - #[cfg(feature = "stories")] pub use stories::*; @@ -158,29 +81,38 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{action, Div, Render}; use itertools::Itertools; pub struct KeybindingStory; + #[action] + struct NoAction {} + + pub fn binding(key: &str) -> gpui::KeyBinding { + gpui::KeyBinding::new(key, NoAction {}, None) + } + impl Render for KeybindingStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let all_modifier_permutations = ModifierKey::iter().permutations(2); + let all_modifier_permutations = + ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2); Story::container(cx) - .child(Story::title_for::<_, Keybinding>(cx)) + .child(Story::title_for::<_, KeyBinding>(cx)) .child(Story::label(cx, "Single Key")) - .child(Keybinding::new("Z".to_string(), ModifierKeys::new())) + .child(KeyBinding::new(binding("Z"))) .child(Story::label(cx, "Single Key with Modifier")) .child( div() .flex() .gap_3() - .children(ModifierKey::iter().map(|modifier| { - Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier)) - })), + .child(KeyBinding::new(binding("ctrl-c"))) + .child(KeyBinding::new(binding("alt-c"))) + .child(KeyBinding::new(binding("cmd-c"))) + .child(KeyBinding::new(binding("shift-c"))), ) .child(Story::label(cx, "Single Key with Modifier (Permuted)")) .child( @@ -194,29 +126,18 @@ mod stories { .gap_4() .py_3() .children(chunk.map(|permutation| { - let mut modifiers = ModifierKeys::new(); - - for modifier in permutation { - modifiers = modifiers.add(modifier); - } - - Keybinding::new("X".to_string(), modifiers) + KeyBinding::new(binding(&*(permutation.join("-") + "-x"))) })) }), ), ) .child(Story::label(cx, "Single Key with All Modifiers")) - .child(Keybinding::new("Z".to_string(), ModifierKeys::all())) + .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z"))) .child(Story::label(cx, "Chord")) - .child(Keybinding::new_chord( - ("A".to_string(), ModifierKeys::new()), - ("Z".to_string(), ModifierKeys::new()), - )) + .child(KeyBinding::new(binding("a z"))) .child(Story::label(cx, "Chord with Modifier")) - .child(Keybinding::new_chord( - ("A".to_string(), ModifierKeys::new().control(true)), - ("Z".to_string(), ModifierKeys::new().shift(true)), - )) + .child(KeyBinding::new(binding("ctrl-a shift-z"))) + .child(KeyBinding::new(binding("fn-s"))) } } } diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 827ba87918a0cd4dd3f3255c8b7852436ea06a79..6b915af1b9bbcbe2b6fab7c8493861ac3b06d7bf 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -1,5 +1,4 @@ -use gpui::{relative, Hsla, WindowContext}; -use smallvec::SmallVec; +use gpui::{relative, Hsla, Text, TextRun, WindowContext}; use crate::prelude::*; use crate::styled_ext::StyledExt; @@ -105,6 +104,8 @@ pub struct HighlightedLabel { } impl HighlightedLabel { + /// shows a label with the given characters highlighted. + /// characters are identified by utf8 byte position. pub fn new(label: impl Into, highlight_indices: Vec) -> Self { Self { label: label.into(), @@ -126,10 +127,11 @@ impl HighlightedLabel { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let highlight_color = cx.theme().colors().text_accent; + let mut text_style = cx.text_style().clone(); let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); - let mut runs: SmallVec<[Run; 8]> = SmallVec::new(); + let mut runs: Vec = Vec::new(); for (char_ix, char) in self.label.char_indices() { let mut color = self.color.hsla(cx); @@ -137,16 +139,14 @@ impl HighlightedLabel { if let Some(highlight_ix) = highlight_indices.peek() { if char_ix == *highlight_ix { color = highlight_color; - highlight_indices.next(); } } let last_run = runs.last_mut(); - let start_new_run = if let Some(last_run) = last_run { if color == last_run.color { - last_run.text.push(char); + last_run.len += char.len_utf8(); false } else { true @@ -156,10 +156,8 @@ impl HighlightedLabel { }; if start_new_run { - runs.push(Run { - text: char.to_string(), - color, - }); + text_style.color = color; + runs.push(text_style.to_run(char.len_utf8())) } } @@ -176,10 +174,7 @@ impl HighlightedLabel { .bg(LabelColor::Hidden.hsla(cx)), ) }) - .children( - runs.into_iter() - .map(|run| div().text_color(run.color).child(run.text)), - ) + .child(Text::styled(self.label, runs)) } } @@ -213,6 +208,10 @@ mod stories { "Hello, world!", vec![0, 1, 2, 7, 8, 12], )) + .child(HighlightedLabel::new( + "Héllo, world!", + vec![0, 1, 3, 8, 9, 13], + )) } } } diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 249e577ff1048efd305b659a535ec04d6ebcc93d..c72722dc086cb8c2d02370c9261549d65689232d 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -1,4 +1,4 @@ -use crate::{h_stack, prelude::*, v_stack, Keybinding, Label, LabelColor}; +use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label, LabelColor}; use gpui::prelude::*; #[derive(Component)] @@ -108,7 +108,7 @@ impl Palette { pub struct PaletteItem { pub label: SharedString, pub sublabel: Option, - pub keybinding: Option, + pub keybinding: Option, } impl PaletteItem { @@ -132,7 +132,7 @@ impl PaletteItem { pub fn keybinding(mut self, keybinding: K) -> Self where - K: Into>, + K: Into>, { self.keybinding = keybinding.into(); self @@ -161,7 +161,7 @@ pub use stories::*; mod stories { use gpui::{Div, Render}; - use crate::{ModifierKeys, Story}; + use crate::{binding, Story}; use super::*; @@ -181,46 +181,24 @@ mod stories { Palette::new("palette-2") .placeholder("Execute a command...") .items(vec![ - PaletteItem::new("theme selector: toggle").keybinding( - Keybinding::new_chord( - ("k".to_string(), ModifierKeys::new().command(true)), - ("t".to_string(), ModifierKeys::new().command(true)), - ), - ), - PaletteItem::new("assistant: inline assist").keybinding( - Keybinding::new( - "enter".to_string(), - ModifierKeys::new().command(true), - ), - ), - PaletteItem::new("assistant: quote selection").keybinding( - Keybinding::new( - ">".to_string(), - ModifierKeys::new().command(true), - ), - ), - PaletteItem::new("assistant: toggle focus").keybinding( - Keybinding::new( - "?".to_string(), - ModifierKeys::new().command(true), - ), - ), + PaletteItem::new("theme selector: toggle") + .keybinding(KeyBinding::new(binding("cmd-k cmd-t"))), + PaletteItem::new("assistant: inline assist") + .keybinding(KeyBinding::new(binding("cmd-enter"))), + PaletteItem::new("assistant: quote selection") + .keybinding(KeyBinding::new(binding("cmd-<"))), + PaletteItem::new("assistant: toggle focus") + .keybinding(KeyBinding::new(binding("cmd-?"))), PaletteItem::new("auto update: check"), PaletteItem::new("auto update: view release notes"), - PaletteItem::new("branches: open recent").keybinding( - Keybinding::new( - "b".to_string(), - ModifierKeys::new().command(true).alt(true), - ), - ), + PaletteItem::new("branches: open recent") + .keybinding(KeyBinding::new(binding("cmd-alt-b"))), PaletteItem::new("chat panel: toggle focus"), PaletteItem::new("cli: install"), PaletteItem::new("client: sign in"), PaletteItem::new("client: sign out"), - PaletteItem::new("editor: cancel").keybinding(Keybinding::new( - "escape".to_string(), - ModifierKeys::new(), - )), + PaletteItem::new("editor: cancel") + .keybinding(KeyBinding::new(binding("escape"))), ]), ) } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 80c6863f6815cd97aa63b81f890ed337000ea3f8..10316c609a14bb808da4a5dd3771f6f26c0b60aa 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,6 +1,8 @@ use gpui::{div, Div, ParentComponent, Render, SharedString, Styled, ViewContext}; use theme2::ActiveTheme; +use crate::StyledExt; + #[derive(Clone, Debug)] pub struct TextTooltip { title: SharedString, @@ -16,16 +18,13 @@ impl Render for TextTooltip { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let theme = cx.theme(); div() - .bg(theme.colors().background) - .rounded_lg() - .border() + .elevation_2(cx) .font("Zed Sans") - .border_color(theme.colors().border) - .text_color(theme.colors().text) - .pl_2() - .pr_2() + .text_ui() + .text_color(cx.theme().colors().text) + .py_1() + .px_2() .child(self.title.clone()) } } diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index ffdd3fee9849147719f89dcf08c23bff17dee4d9..89aef8140a062685b59f00c508d99fea5fe19b1b 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -7,12 +7,12 @@ use gpui::{AppContext, ViewContext}; use rand::Rng; use theme2::ActiveTheme; -use crate::HighlightedText; +use crate::{binding, HighlightedText}; use crate::{ Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus, - HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, - MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus, - PlayerWithCallStatus, PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, + HighlightedLine, Icon, KeyBinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, + MicStatus, Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, + PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, }; use crate::{ListItem, NotificationAction}; @@ -701,46 +701,16 @@ pub fn static_collab_panel_channels() -> Vec { pub fn example_editor_actions() -> Vec { vec![ - PaletteItem::new("New File").keybinding(Keybinding::new( - "N".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Open File").keybinding(Keybinding::new( - "O".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Save File").keybinding(Keybinding::new( - "S".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Cut").keybinding(Keybinding::new( - "X".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Copy").keybinding(Keybinding::new( - "C".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Paste").keybinding(Keybinding::new( - "V".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Undo").keybinding(Keybinding::new( - "Z".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Redo").keybinding(Keybinding::new( - "Z".to_string(), - ModifierKeys::new().command(true).shift(true), - )), - PaletteItem::new("Find").keybinding(Keybinding::new( - "F".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Replace").keybinding(Keybinding::new( - "R".to_string(), - ModifierKeys::new().command(true), - )), + PaletteItem::new("New File").keybinding(KeyBinding::new(binding("cmd-n"))), + PaletteItem::new("Open File").keybinding(KeyBinding::new(binding("cmd-o"))), + PaletteItem::new("Save File").keybinding(KeyBinding::new(binding("cmd-s"))), + PaletteItem::new("Cut").keybinding(KeyBinding::new(binding("cmd-x"))), + PaletteItem::new("Copy").keybinding(KeyBinding::new(binding("cmd-c"))), + PaletteItem::new("Paste").keybinding(KeyBinding::new(binding("cmd-v"))), + PaletteItem::new("Undo").keybinding(KeyBinding::new(binding("cmd-z"))), + PaletteItem::new("Redo").keybinding(KeyBinding::new(binding("cmd-shift-z"))), + PaletteItem::new("Find").keybinding(KeyBinding::new(binding("cmd-f"))), + PaletteItem::new("Replace").keybinding(KeyBinding::new(binding("cmd-r"))), PaletteItem::new("Jump to Line"), PaletteItem::new("Select All"), PaletteItem::new("Deselect All"), diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 3ebb2749692ccb5401b5af26320e8f976a55c69f..0703d0dc563205dd9ee97c3ea9ab484a624fd198 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -2,7 +2,7 @@ use gpui::{ div, prelude::*, px, AnyView, EventEmitter, FocusHandle, Div, Render, Subscription, View, ViewContext, WindowContext, }; -use ui::v_stack; +use ui::{h_stack, v_stack}; pub struct ActiveModal { modal: AnyView, @@ -33,8 +33,6 @@ impl ModalLayer { V: Modal, B: FnOnce(&mut ViewContext) -> V, { - let previous_focus = cx.focused(); - if let Some(active_modal) = &self.active_modal { let is_close = active_modal.modal.clone().downcast::().is_ok(); self.hide_modal(cx); @@ -85,9 +83,6 @@ impl Render for ModalLayer { div() .absolute() - .flex() - .flex_col() - .items_center() .size_full() .top_0() .left_0() @@ -96,11 +91,21 @@ impl Render for ModalLayer { v_stack() .h(px(0.0)) .top_20() + .flex() + .flex_col() + .items_center() .track_focus(&active_modal.focus_handle) - .on_mouse_down_out(|this: &mut Self, event, cx| { - this.hide_modal(cx); - }) - .child(active_modal.modal.clone()), + .child( + h_stack() + // needed to prevent mouse events leaking to the + // UI below. // todo! for gpui3. + .on_any_mouse_down(|_, _, cx| cx.stop_propagation()) + .on_any_mouse_up(|_, _, cx| cx.stop_propagation()) + .on_mouse_down_out(|this: &mut Self, event, cx| { + this.hide_modal(cx); + }) + .child(active_modal.modal.clone()), + ), ) } } diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index c4935599a60167b138194b0f3386d61e0f017763..aeca6173428dbedfeedac4f3437d82ab11b6010e 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1399,20 +1399,32 @@ impl Pane { // .on_drop(|_view, state: View, cx| { // eprintln!("{:?}", state.read(cx)); // }) - .px_2() - .py_0p5() .flex() .items_center() .justify_center() + // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize") + .map(|this| { + if close_right { + this.pl_3().pr_1() + } else { + this.pr_1().pr_3() + } + }) + .py_1() .bg(tab_bg) - .hover(|h| h.bg(tab_hover_bg)) - .active(|a| a.bg(tab_active_bg)) + .border_color(cx.theme().colors().border) + .map(|this| match ix.cmp(&self.active_item_index) { + cmp::Ordering::Less => this.border_l(), + cmp::Ordering::Equal => this.border_r(), + cmp::Ordering::Greater => this.border_l().border_r(), + }) + // .hover(|h| h.bg(tab_hover_bg)) + // .active(|a| a.bg(tab_active_bg)) .child( div() - .px_1() .flex() .items_center() - .gap_1p5() + .gap_1() .text_color(text_color) .children(if item.has_conflict(cx) { Some( diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 10399624b58f180a57441f724a7e27e2e8383997..b48e4aa27855dc0e20b132ca5a2fb08fad1af641 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -1311,53 +1311,56 @@ impl Workspace { // })) // } - // pub fn prepare_to_close( - // &mut self, - // quitting: bool, - // cx: &mut ViewContext, - // ) -> Task> { - // let active_call = self.active_call().cloned(); - // let window = cx.window(); - - // cx.spawn(|this, mut cx| async move { - // let workspace_count = cx - // .windows() - // .into_iter() - // .filter(|window| window.root_is::()) - // .count(); - - // if let Some(active_call) = active_call { - // if !quitting - // && workspace_count == 1 - // && active_call.read_with(&cx, |call, _| call.room().is_some()) - // { - // let answer = window.prompt( - // PromptLevel::Warning, - // "Do you want to leave the current call?", - // &["Close window and hang up", "Cancel"], - // &mut cx, - // ); - - // if let Some(mut answer) = answer { - // if answer.next().await == Some(1) { - // return anyhow::Ok(false); - // } else { - // active_call - // .update(&mut cx, |call, cx| call.hang_up(cx)) - // .await - // .log_err(); - // } - // } - // } - // } + pub fn prepare_to_close( + &mut self, + quitting: bool, + cx: &mut ViewContext, + ) -> Task> { + //todo!(saveing) + // let active_call = self.active_call().cloned(); + // let window = cx.window(); + + cx.spawn(|this, mut cx| async move { + // let workspace_count = cx + // .windows() + // .into_iter() + // .filter(|window| window.root_is::()) + // .count(); + + // if let Some(active_call) = active_call { + // if !quitting + // && workspace_count == 1 + // && active_call.read_with(&cx, |call, _| call.room().is_some()) + // { + // let answer = window.prompt( + // PromptLevel::Warning, + // "Do you want to leave the current call?", + // &["Close window and hang up", "Cancel"], + // &mut cx, + // ); + + // if let Some(mut answer) = answer { + // if answer.next().await == Some(1) { + // return anyhow::Ok(false); + // } else { + // active_call + // .update(&mut cx, |call, cx| call.hang_up(cx)) + // .await + // .log_err(); + // } + // } + // } + // } - // Ok(this - // .update(&mut cx, |this, cx| { - // this.save_all_internal(SaveIntent::Close, cx) - // })? - // .await?) - // }) - // } + Ok( + false, // this + // .update(&mut cx, |this, cx| { + // this.save_all_internal(SaveIntent::Close, cx) + // })? + // .await? + ) + }) + } // fn save_all( // &mut self, @@ -4189,24 +4192,24 @@ impl ViewId { } } -// pub trait WorkspaceHandle { -// fn file_project_paths(&self, cx: &AppContext) -> Vec; -// } +pub trait WorkspaceHandle { + fn file_project_paths(&self, cx: &AppContext) -> Vec; +} -// impl WorkspaceHandle for View { -// fn file_project_paths(&self, cx: &AppContext) -> Vec { -// self.read(cx) -// .worktrees(cx) -// .flat_map(|worktree| { -// let worktree_id = worktree.read(cx).id(); -// worktree.read(cx).files(true, 0).map(move |f| ProjectPath { -// worktree_id, -// path: f.path.clone(), -// }) -// }) -// .collect::>() -// } -// } +impl WorkspaceHandle for View { + fn file_project_paths(&self, cx: &AppContext) -> Vec { + self.read(cx) + .worktrees(cx) + .flat_map(|worktree| { + let worktree_id = worktree.read(cx).id(); + worktree.read(cx).files(true, 0).map(move |f| ProjectPath { + worktree_id, + path: f.path.clone(), + }) + }) + .collect::>() + } +} // impl std::fmt::Debug for OpenPaths { // fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index c9e7ee8c580eb4f4b694855376d3b632c4fb22a8..2deaff21491a73bd54bcf5d80a0d2ca9c7a76a7f 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -9,6 +9,7 @@ use backtrace::Backtrace; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::UserStore; use db::kvp::KEY_VALUE_STORE; +use editor::Editor; use fs::RealFs; use futures::StreamExt; use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; @@ -56,10 +57,7 @@ use zed2::{ mod open_listener; fn main() { - //TODO!(figure out what the linker issues are here) - // https://github.com/rust-lang/rust/issues/47384 - // https://github.com/mmastrac/rust-ctor/issues/280 - menu::unused(); + menu::init(); let http = http::client(); init_paths(); init_logger(); @@ -357,8 +355,7 @@ async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncApp } else { cx.update(|cx| { workspace::open_new(app_state, cx, |workspace, cx| { - // todo!(editor) - // Editor::new_file(workspace, &Default::default(), cx) + Editor::new_file(workspace, &Default::default(), cx) }) .detach(); })?; diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 7368d3a5efe22319bc398d72af514a746a6fdc39..de985496c8eeb30a6359aedac3122adf2d6dc816 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -55,7 +55,7 @@ pub fn initialize_workspace( ) -> Task> { cx.spawn(|mut cx| async move { workspace_handle.update(&mut cx, |workspace, cx| { - let workspace_handle = cx.view(); + let workspace_handle = cx.view().clone(); cx.subscribe(&workspace_handle, { move |workspace, _, event, cx| { if let workspace::Event::PaneAdded(pane) = event {