diff --git a/crates/collab2/src/tests.rs b/crates/collab2/src/tests.rs index cb25856551..a669f260db 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/editor_tests.rs b/crates/collab2/src/tests/editor_tests.rs index 4900cb20f6..6e84780466 100644 --- a/crates/collab2/src/tests/editor_tests.rs +++ b/crates/collab2/src/tests/editor_tests.rs @@ -1,1108 +1,1110 @@ -// use editor::{ -// test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, -// ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo, -// }; - -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_host_disconnect( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// 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; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) -// .await; - -// cx_b.update(editor::init); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "a.txt": "a-contents", -// "b.txt": "b-contents", -// }), -// ) -// .await; - -// 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 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; -// executor.run_until_parked(); - -// assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - -// let window_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 editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "b.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx))); -// editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); -// assert!(window_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(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// project_a.read_with(cx_a, |project, _| project.collaborators().is_empty()); - -// project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); - -// project_b.read_with(cx_b, |project, _| project.is_read_only()); - -// assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - -// // Ensure client B's edited state is reset and that the whole window is blurred. - -// window_b.read_with(cx_b, |cx| { -// assert_eq!(cx.focused_view_id(), None); -// }); -// assert!(!window_b.is_edited(cx_b)); - -// // Ensure client B is not prompted to save edits when closing window after disconnecting. -// let can_close = workspace_b -// .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx)) -// .await -// .unwrap(); -// assert!(can_close); - -// // Allow client A to reconnect to the server. -// server.allow_connections(); -// executor.advance_clock(RECEIVE_TIMEOUT); - -// // Client B calls client A again after they reconnected. -// let active_call_b = cx_b.read(ActiveCall::global); -// active_call_b -// .update(cx_b, |call, cx| { -// call.invite(client_a.user_id().unwrap(), None, cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// active_call_a -// .update(cx_a, |call, cx| call.accept_incoming(cx)) -// .await -// .unwrap(); - -// active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// // Drop client A's connection again. We should still unshare it successfully. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// 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, -// 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); - -// client_a -// .fs() -// .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/dir", 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 buffer as client A -// let buffer_a = project_a -// .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 mut editor_cx_a = EditorTestContext { -// cx: cx_a, -// window: window_a.into(), -// editor: editor_a, -// }; - -// // Open a buffer as client B -// let buffer_b = project_b -// .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 mut editor_cx_b = EditorTestContext { -// cx: cx_b, -// window: window_b.into(), -// editor: editor_b, -// }; - -// // Test newline above -// editor_cx_a.set_selections_state(indoc! {" -// Some textˇ -// "}); -// editor_cx_b.set_selections_state(indoc! {" -// Some textˇ -// "}); -// editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx)); -// executor.run_until_parked(); -// editor_cx_a.assert_editor_state(indoc! {" -// ˇ -// Some text -// "}); -// editor_cx_b.assert_editor_state(indoc! {" - -// Some textˇ -// "}); - -// // Test newline below -// editor_cx_a.set_selections_state(indoc! {" - -// Some textˇ -// "}); -// editor_cx_b.set_selections_state(indoc! {" - -// Some textˇ -// "}); -// editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx)); -// executor.run_until_parked(); -// editor_cx_a.assert_editor_state(indoc! {" - -// Some text -// ˇ -// "}); -// editor_cx_b.assert_editor_state(indoc! {" - -// Some textˇ - -// "}); -// } - -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_collaborating_with_completion( -// 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 { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string()]), -// resolve_provider: Some(true), -// ..Default::default() -// }), -// ..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": "", -// }), -// ) -// .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.clone(), Some(project_b.clone()), cx) -// }); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// cx_a.foreground().run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert!(!buffer.completion_triggers().is_empty()) -// }); - -// // Type a completion 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 a completion request as the host's language server. -// // Return some completions 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(lsp::CompletionResponse::Array(vec![ -// lsp::CompletionItem { -// label: "first_method(…)".into(), -// detail: Some("fn(&mut self, B) -> C".into()), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// new_text: "first_method($1)".to_string(), -// range: lsp::Range::new( -// lsp::Position::new(0, 14), -// lsp::Position::new(0, 14), -// ), -// })), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// ..Default::default() -// }, -// lsp::CompletionItem { -// label: "second_method(…)".into(), -// detail: Some("fn(&mut self, C) -> D".into()), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// new_text: "second_method()".to_string(), -// range: lsp::Range::new( -// lsp::Position::new(0, 14), -// lsp::Position::new(0, 14), -// ), -// })), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// ..Default::default() -// }, -// ]))) -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); - -// // Open the buffer on the host. -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// cx_a.foreground().run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a. }") -// }); - -// // Confirm a completion on the guest. - -// editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); -// editor_b.update(cx_b, |editor, cx| { -// editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); -// assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); -// }); - -// // Return a resolved completion from the host's language server. -// // The resolved completion has an additional text edit. -// fake_language_server.handle_request::( -// |params, _| async move { -// assert_eq!(params.label, "first_method(…)"); -// Ok(lsp::CompletionItem { -// label: "first_method(…)".into(), -// detail: Some("fn(&mut self, B) -> C".into()), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// new_text: "first_method($1)".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// })), -// additional_text_edits: Some(vec![lsp::TextEdit { -// new_text: "use d::SomeTrait;\n".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), -// }]), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// ..Default::default() -// }) -// }, -// ); - -// // The additional edit is applied. -// cx_a.foreground().run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!( -// buffer.text(), -// "use d::SomeTrait;\nfn main() { a.first_method() }" -// ); -// }); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!( -// buffer.text(), -// "use d::SomeTrait;\nfn main() { a.first_method() }" -// ); -// }); -// } -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_collaborating_with_code_actions( -// 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); - -// cx_b.update(editor::init); - -// // 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(Default::default()).await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", -// "other.rs": "pub fn foo() -> usize { 4 }", -// }), -// ) -// .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(); - -// // Join the project as client B. -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// let window_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 editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let mut fake_language_server = fake_language_servers.next().await.unwrap(); -// let mut requests = fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!(params.range.start, lsp::Position::new(0, 0)); -// assert_eq!(params.range.end, lsp::Position::new(0, 0)); -// Ok(None) -// }); -// executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); -// requests.next().await; - -// // Move cursor to a location that contains code actions. -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 31)..Point::new(1, 31)]) -// }); -// cx.focus(&editor_b); -// }); - -// let mut requests = fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!(params.range.start, lsp::Position::new(1, 31)); -// assert_eq!(params.range.end, lsp::Position::new(1, 31)); - -// Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( -// lsp::CodeAction { -// title: "Inline into all callers".to_string(), -// edit: Some(lsp::WorkspaceEdit { -// changes: Some( -// [ -// ( -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(1, 22), -// lsp::Position::new(1, 34), -// ), -// "4".to_string(), -// )], -// ), -// ( -// lsp::Url::from_file_path("/a/other.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 0), -// lsp::Position::new(0, 27), -// ), -// "".to_string(), -// )], -// ), -// ] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// }), -// data: Some(json!({ -// "codeActionParams": { -// "range": { -// "start": {"line": 1, "column": 31}, -// "end": {"line": 1, "column": 31}, -// } -// } -// })), -// ..Default::default() -// }, -// )])) -// }); -// executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); -// requests.next().await; - -// // Toggle code actions and wait for them to display. -// editor_b.update(cx_b, |editor, cx| { -// editor.toggle_code_actions( -// &ToggleCodeActions { -// deployed_from_indicator: false, -// }, -// cx, -// ); -// }); -// cx_a.foreground().run_until_parked(); - -// editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); - -// fake_language_server.remove_request_handler::(); - -// // Confirming the code action will trigger a resolve request. -// let confirm_action = workspace_b -// .update(cx_b, |workspace, cx| { -// Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx) -// }) -// .unwrap(); -// fake_language_server.handle_request::( -// |_, _| async move { -// Ok(lsp::CodeAction { -// title: "Inline into all callers".to_string(), -// edit: Some(lsp::WorkspaceEdit { -// changes: Some( -// [ -// ( -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(1, 22), -// lsp::Position::new(1, 34), -// ), -// "4".to_string(), -// )], -// ), -// ( -// lsp::Url::from_file_path("/a/other.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 0), -// lsp::Position::new(0, 27), -// ), -// "".to_string(), -// )], -// ), -// ] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// }), -// ..Default::default() -// }) -// }, -// ); - -// // After the action is confirmed, an editor containing both modified files is opened. -// confirm_action.await.unwrap(); - -// let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// code_action_editor.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); -// editor.undo(&Undo, cx); -// assert_eq!( -// editor.text(cx), -// "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }" -// ); -// editor.redo(&Redo, cx); -// assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); -// }); -// } - -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_collaborating_with_renames( -// 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); - -// cx_b.update(editor::init); - -// // 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 { -// rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { -// prepare_provider: Some(true), -// work_done_progress_options: Default::default(), -// })), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/dir", -// json!({ -// "one.rs": "const ONE: usize = 1;", -// "two.rs": "const TWO: usize = one::ONE + one::ONE;" -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/dir", 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; - -// let window_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 editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "one.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let fake_language_server = fake_language_servers.next().await.unwrap(); - -// // Move cursor to a location that can be renamed. -// let prepare_rename = editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([7..7])); -// editor.rename(&Rename, cx).unwrap() -// }); - -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); -// assert_eq!(params.position, lsp::Position::new(0, 7)); -// Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( -// lsp::Position::new(0, 6), -// lsp::Position::new(0, 9), -// )))) -// }) -// .next() -// .await -// .unwrap(); -// prepare_rename.await.unwrap(); -// editor_b.update(cx_b, |editor, cx| { -// use editor::ToOffset; -// let rename = editor.pending_rename().unwrap(); -// let buffer = editor.buffer().read(cx).snapshot(cx); -// assert_eq!( -// rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer), -// 6..9 -// ); -// rename.editor.update(cx, |rename_editor, cx| { -// rename_editor.buffer().update(cx, |rename_buffer, cx| { -// rename_buffer.edit([(0..3, "THREE")], None, cx); -// }); -// }); -// }); - -// let confirm_rename = workspace_b.update(cx_b, |workspace, cx| { -// Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap() -// }); -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri.as_str(), -// "file:///dir/one.rs" -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 6) -// ); -// assert_eq!(params.new_name, "THREE"); -// Ok(Some(lsp::WorkspaceEdit { -// changes: Some( -// [ -// ( -// lsp::Url::from_file_path("/dir/one.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), -// "THREE".to_string(), -// )], -// ), -// ( -// lsp::Url::from_file_path("/dir/two.rs").unwrap(), -// vec![ -// lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 24), -// lsp::Position::new(0, 27), -// ), -// "THREE".to_string(), -// ), -// lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 35), -// lsp::Position::new(0, 38), -// ), -// "THREE".to_string(), -// ), -// ], -// ), -// ] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// })) -// }) -// .next() -// .await -// .unwrap(); -// confirm_rename.await.unwrap(); - -// let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// rename_editor.update(cx_b, |editor, cx| { -// assert_eq!( -// editor.text(cx), -// "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" -// ); -// editor.undo(&Undo, cx); -// assert_eq!( -// editor.text(cx), -// "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;" -// ); -// editor.redo(&Redo, cx); -// assert_eq!( -// editor.text(cx), -// "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" -// ); -// }); - -// // Ensure temporary rename edits cannot be undone/redone. -// editor_b.update(cx_b, |editor, cx| { -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "const ONE: usize = 1;"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "const ONE: usize = 1;"); -// editor.redo(&Redo, cx); -// assert_eq!(editor.text(cx), "const THREE: usize = 1;"); -// }) -// } - -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_language_server_statuses( -// 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); - -// cx_b.update(editor::init); - -// // 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 { -// name: "the-language-server", -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/dir", -// json!({ -// "main.rs": "const ONE: usize = 1;", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - -// let _buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// fake_language_server.start_progress("the-token").await; -// fake_language_server.notify::(lsp::ProgressParams { -// token: lsp::NumberOrString::String("the-token".to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( -// lsp::WorkDoneProgressReport { -// message: Some("the-message".to_string()), -// ..Default::default() -// }, -// )), -// }); -// executor.run_until_parked(); - -// project_a.read_with(cx_a, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// assert_eq!(status.pending_work.len(), 1); -// assert_eq!( -// status.pending_work["the-token"].message.as_ref().unwrap(), -// "the-message" -// ); -// }); - -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; - -// project_b.read_with(cx_b, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// }); - -// fake_language_server.notify::(lsp::ProgressParams { -// token: lsp::NumberOrString::String("the-token".to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( -// lsp::WorkDoneProgressReport { -// message: Some("the-message-2".to_string()), -// ..Default::default() -// }, -// )), -// }); -// executor.run_until_parked(); - -// project_a.read_with(cx_a, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// assert_eq!(status.pending_work.len(), 1); -// assert_eq!( -// status.pending_work["the-token"].message.as_ref().unwrap(), -// "the-message-2" -// ); -// }); - -// project_b.read_with(cx_b, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// assert_eq!(status.pending_work.len(), 1); -// assert_eq!( -// status.pending_work["the-token"].message.as_ref().unwrap(), -// "the-message-2" -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_share_project( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// ) { -// let window_b = cx_b.add_window(|_| EmptyView); -// 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; -// server -// .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) -// .await; -// 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); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// ".gitignore": "ignored-dir", -// "a.txt": "a-contents", -// "b.txt": "b-contents", -// "ignored-dir": { -// "c.txt": "", -// "d.txt": "", -// } -// }), -// ) -// .await; - -// // Invite client B to collaborate on a project -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| { -// call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx) -// }) -// .await -// .unwrap(); - -// // Join that project as client B - -// let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); -// executor.run_until_parked(); -// let call = incoming_call_b.borrow().clone().unwrap(); -// assert_eq!(call.calling_user.github_login, "user_a"); -// let initial_project = call.initial_project.unwrap(); -// active_call_b -// .update(cx_b, |call, cx| call.accept_incoming(cx)) -// .await -// .unwrap(); -// let client_b_peer_id = client_b.peer_id().unwrap(); -// let project_b = client_b -// .build_remote_project(initial_project.id, cx_b) -// .await; - -// let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); - -// executor.run_until_parked(); - -// project_a.read_with(cx_a, |project, _| { -// let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap(); -// assert_eq!(client_b_collaborator.replica_id, replica_id_b); -// }); - -// project_b.read_with(cx_b, |project, cx| { -// let worktree = project.worktrees().next().unwrap().read(cx); -// assert_eq!( -// worktree.paths().map(AsRef::as_ref).collect::>(), -// [ -// Path::new(".gitignore"), -// Path::new("a.txt"), -// Path::new("b.txt"), -// Path::new("ignored-dir"), -// ] -// ); -// }); - -// project_b -// .update(cx_b, |project, cx| { -// let worktree = project.worktrees().next().unwrap(); -// let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap(); -// project.expand_entry(worktree_id, entry.id, cx).unwrap() -// }) -// .await -// .unwrap(); - -// project_b.read_with(cx_b, |project, cx| { -// let worktree = project.worktrees().next().unwrap().read(cx); -// assert_eq!( -// worktree.paths().map(AsRef::as_ref).collect::>(), -// [ -// Path::new(".gitignore"), -// Path::new("a.txt"), -// Path::new("b.txt"), -// Path::new("ignored-dir"), -// Path::new("ignored-dir/c.txt"), -// Path::new("ignored-dir/d.txt"), -// ] -// ); -// }); - -// // Open the same file as client B and client A. -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) -// .await -// .unwrap(); - -// buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); - -// project_a.read_with(cx_a, |project, cx| { -// assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) -// }); -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) -// .await -// .unwrap(); - -// let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); - -// // Client A sees client B's selection -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// buffer -// .snapshot() -// .remote_selections_in_range(Anchor::MIN..Anchor::MAX) -// .count() -// == 1 -// }); - -// // Edit the buffer as client B and see that edit as client A. -// editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx)); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "ok, b-contents") -// }); - -// // Client B can invite client C on a project shared by client A. -// active_call_b -// .update(cx_b, |call, cx| { -// call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx) -// }) -// .await -// .unwrap(); - -// let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); -// executor.run_until_parked(); -// let call = incoming_call_c.borrow().clone().unwrap(); -// assert_eq!(call.calling_user.github_login, "user_b"); -// let initial_project = call.initial_project.unwrap(); -// active_call_c -// .update(cx_c, |call, cx| call.accept_incoming(cx)) -// .await -// .unwrap(); -// let _project_c = client_c -// .build_remote_project(initial_project.id, cx_c) -// .await; - -// // Client B closes the editor, and client A sees client B's selections removed. -// cx_b.update(move |_| drop(editor_b)); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// buffer -// .snapshot() -// .remote_selections_in_range(Anchor::MIN..Anchor::MAX) -// .count() -// == 0 -// }); -// } +use editor::{ + test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, + ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo, +}; +use gpui::{BackgroundExecutor, TestAppContext}; + +use crate::tests::TestServer; + +#[gpui::test(iterations = 10)] +async fn test_host_disconnect( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + 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; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + + 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 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; + executor.run_until_parked(); + + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + + let window_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 editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "b.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx))); + editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); + assert!(window_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(); + server.disconnect_client(client_a.peer_id().unwrap()); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + project_a.read_with(cx_a, |project, _| project.collaborators().is_empty()); + + project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); + + project_b.read_with(cx_b, |project, _| project.is_read_only()); + + assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); + + // Ensure client B's edited state is reset and that the whole window is blurred. + + window_b.read_with(cx_b, |cx| { + assert_eq!(cx.focused_view_id(), None); + }); + assert!(!window_b.is_edited(cx_b)); + + // Ensure client B is not prompted to save edits when closing window after disconnecting. + let can_close = workspace_b + .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx)) + .await + .unwrap(); + assert!(can_close); + + // Allow client A to reconnect to the server. + server.allow_connections(); + executor.advance_clock(RECEIVE_TIMEOUT); + + // Client B calls client A again after they reconnected. + let active_call_b = cx_b.read(ActiveCall::global); + active_call_b + .update(cx_b, |call, cx| { + call.invite(client_a.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + active_call_a + .update(cx_a, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + + active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Drop client A's connection again. We should still unshare it successfully. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + 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, + 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); + + client_a + .fs() + .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", 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 buffer as client A + let buffer_a = project_a + .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 mut editor_cx_a = EditorTestContext { + cx: cx_a, + window: window_a.into(), + editor: editor_a, + }; + + // Open a buffer as client B + let buffer_b = project_b + .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 mut editor_cx_b = EditorTestContext { + cx: cx_b, + window: window_b.into(), + editor: editor_b, + }; + + // Test newline above + editor_cx_a.set_selections_state(indoc! {" + Some textˇ + "}); + editor_cx_b.set_selections_state(indoc! {" + Some textˇ + "}); + editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx)); + executor.run_until_parked(); + editor_cx_a.assert_editor_state(indoc! {" + ˇ + Some text + "}); + editor_cx_b.assert_editor_state(indoc! {" + + Some textˇ + "}); + + // Test newline below + editor_cx_a.set_selections_state(indoc! {" + + Some textˇ + "}); + editor_cx_b.set_selections_state(indoc! {" + + Some textˇ + "}); + editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx)); + executor.run_until_parked(); + editor_cx_a.assert_editor_state(indoc! {" + + Some text + ˇ + "}); + editor_cx_b.assert_editor_state(indoc! {" + + Some textˇ + + "}); +} + +todo!(editor) +#[gpui::test(iterations = 10)] +async fn test_collaborating_with_completion( + 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 { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..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": "", + }), + ) + .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.clone(), Some(project_b.clone()), cx) + }); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.foreground().run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert!(!buffer.completion_triggers().is_empty()) + }); + + // Type a completion 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 a completion request as the host's language server. + // Return some completions 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(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "first_method(…)".into(), + detail: Some("fn(&mut self, B) -> C".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "first_method($1)".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + lsp::CompletionItem { + label: "second_method(…)".into(), + detail: Some("fn(&mut self, C) -> D".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "second_method()".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + ]))) + }) + .next() + .await + .unwrap(); + cx_a.foreground().finish_waiting(); + + // Open the buffer on the host. + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a. }") + }); + + // Confirm a completion on the guest. + + editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); + editor_b.update(cx_b, |editor, cx| { + editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); + assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); + }); + + // Return a resolved completion from the host's language server. + // The resolved completion has an additional text edit. + fake_language_server.handle_request::( + |params, _| async move { + assert_eq!(params.label, "first_method(…)"); + Ok(lsp::CompletionItem { + label: "first_method(…)".into(), + detail: Some("fn(&mut self, B) -> C".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "first_method($1)".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + })), + additional_text_edits: Some(vec![lsp::TextEdit { + new_text: "use d::SomeTrait;\n".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), + }]), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }) + }, + ); + + // The additional edit is applied. + cx_a.foreground().run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!( + buffer.text(), + "use d::SomeTrait;\nfn main() { a.first_method() }" + ); + }); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!( + buffer.text(), + "use d::SomeTrait;\nfn main() { a.first_method() }" + ); + }); +} +todo!(editor) +#[gpui::test(iterations = 10)] +async fn test_collaborating_with_code_actions( + 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); + + cx_b.update(editor::init); + + // 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(Default::default()).await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", + "other.rs": "pub fn foo() -> usize { 4 }", + }), + ) + .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(); + + // Join the project as client B. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let window_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 editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let mut requests = fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!(params.range.start, lsp::Position::new(0, 0)); + assert_eq!(params.range.end, lsp::Position::new(0, 0)); + Ok(None) + }); + executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; + + // Move cursor to a location that contains code actions. + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 31)..Point::new(1, 31)]) + }); + cx.focus(&editor_b); + }); + + let mut requests = fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!(params.range.start, lsp::Position::new(1, 31)); + assert_eq!(params.range.end, lsp::Position::new(1, 31)); + + Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "Inline into all callers".to_string(), + edit: Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/a/main.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(1, 22), + lsp::Position::new(1, 34), + ), + "4".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/a/other.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 27), + ), + "".to_string(), + )], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }), + data: Some(json!({ + "codeActionParams": { + "range": { + "start": {"line": 1, "column": 31}, + "end": {"line": 1, "column": 31}, + } + } + })), + ..Default::default() + }, + )])) + }); + executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; + + // Toggle code actions and wait for them to display. + editor_b.update(cx_b, |editor, cx| { + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: false, + }, + cx, + ); + }); + cx_a.foreground().run_until_parked(); + + editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); + + fake_language_server.remove_request_handler::(); + + // Confirming the code action will trigger a resolve request. + let confirm_action = workspace_b + .update(cx_b, |workspace, cx| { + Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx) + }) + .unwrap(); + fake_language_server.handle_request::( + |_, _| async move { + Ok(lsp::CodeAction { + title: "Inline into all callers".to_string(), + edit: Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/a/main.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(1, 22), + lsp::Position::new(1, 34), + ), + "4".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/a/other.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 27), + ), + "".to_string(), + )], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }), + ..Default::default() + }) + }, + ); + + // After the action is confirmed, an editor containing both modified files is opened. + confirm_action.await.unwrap(); + + let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + code_action_editor.update(cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); + editor.undo(&Undo, cx); + assert_eq!( + editor.text(cx), + "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }" + ); + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); + }); +} + +todo!(editor) +#[gpui::test(iterations = 10)] +async fn test_collaborating_with_renames( + 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); + + cx_b.update(editor::init); + + // 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 { + rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: Default::default(), + })), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", 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; + + let window_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 editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "one.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); + + // Move cursor to a location that can be renamed. + let prepare_rename = editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([7..7])); + editor.rename(&Rename, cx).unwrap() + }); + + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); + assert_eq!(params.position, lsp::Position::new(0, 7)); + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + )))) + }) + .next() + .await + .unwrap(); + prepare_rename.await.unwrap(); + editor_b.update(cx_b, |editor, cx| { + use editor::ToOffset; + let rename = editor.pending_rename().unwrap(); + let buffer = editor.buffer().read(cx).snapshot(cx); + assert_eq!( + rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer), + 6..9 + ); + rename.editor.update(cx, |rename_editor, cx| { + rename_editor.buffer().update(cx, |rename_buffer, cx| { + rename_buffer.edit([(0..3, "THREE")], None, cx); + }); + }); + }); + + let confirm_rename = workspace_b.update(cx_b, |workspace, cx| { + Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap() + }); + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri.as_str(), + "file:///dir/one.rs" + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 6) + ); + assert_eq!(params.new_name, "THREE"); + Ok(Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/dir/one.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + "THREE".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/dir/two.rs").unwrap(), + vec![ + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 24), + lsp::Position::new(0, 27), + ), + "THREE".to_string(), + ), + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 35), + lsp::Position::new(0, 38), + ), + "THREE".to_string(), + ), + ], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + })) + }) + .next() + .await + .unwrap(); + confirm_rename.await.unwrap(); + + let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + rename_editor.update(cx_b, |editor, cx| { + assert_eq!( + editor.text(cx), + "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" + ); + editor.undo(&Undo, cx); + assert_eq!( + editor.text(cx), + "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;" + ); + editor.redo(&Redo, cx); + assert_eq!( + editor.text(cx), + "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" + ); + }); + + // Ensure temporary rename edits cannot be undone/redone. + editor_b.update(cx_b, |editor, cx| { + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "const ONE: usize = 1;"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "const ONE: usize = 1;"); + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "const THREE: usize = 1;"); + }) +} + +todo!(editor) +#[gpui::test(iterations = 10)] +async fn test_language_server_statuses( + 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); + + cx_b.update(editor::init); + + // 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 { + name: "the-language-server", + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/dir", + json!({ + "main.rs": "const ONE: usize = 1;", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; + + let _buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.start_progress("the-token").await; + fake_language_server.notify::(lsp::ProgressParams { + token: lsp::NumberOrString::String("the-token".to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( + lsp::WorkDoneProgressReport { + message: Some("the-message".to_string()), + ..Default::default() + }, + )), + }); + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + assert_eq!(status.pending_work.len(), 1); + assert_eq!( + status.pending_work["the-token"].message.as_ref().unwrap(), + "the-message" + ); + }); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + executor.run_until_parked(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + project_b.read_with(cx_b, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + }); + + fake_language_server.notify::(lsp::ProgressParams { + token: lsp::NumberOrString::String("the-token".to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( + lsp::WorkDoneProgressReport { + message: Some("the-message-2".to_string()), + ..Default::default() + }, + )), + }); + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + assert_eq!(status.pending_work.len(), 1); + assert_eq!( + status.pending_work["the-token"].message.as_ref().unwrap(), + "the-message-2" + ); + }); + + project_b.read_with(cx_b, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + assert_eq!(status.pending_work.len(), 1); + assert_eq!( + status.pending_work["the-token"].message.as_ref().unwrap(), + "the-message-2" + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_share_project( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let window_b = cx_b.add_window(|_| EmptyView); + 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; + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + 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); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + ".gitignore": "ignored-dir", + "a.txt": "a-contents", + "b.txt": "b-contents", + "ignored-dir": { + "c.txt": "", + "d.txt": "", + } + }), + ) + .await; + + // Invite client B to collaborate on a project + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx) + }) + .await + .unwrap(); + + // Join that project as client B + + let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); + executor.run_until_parked(); + let call = incoming_call_b.borrow().clone().unwrap(); + assert_eq!(call.calling_user.github_login, "user_a"); + let initial_project = call.initial_project.unwrap(); + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + let client_b_peer_id = client_b.peer_id().unwrap(); + let project_b = client_b + .build_remote_project(initial_project.id, cx_b) + .await; + + let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); + + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, _| { + let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap(); + assert_eq!(client_b_collaborator.replica_id, replica_id_b); + }); + + project_b.read_with(cx_b, |project, cx| { + let worktree = project.worktrees().next().unwrap().read(cx); + assert_eq!( + worktree.paths().map(AsRef::as_ref).collect::>(), + [ + Path::new(".gitignore"), + Path::new("a.txt"), + Path::new("b.txt"), + Path::new("ignored-dir"), + ] + ); + }); + + project_b + .update(cx_b, |project, cx| { + let worktree = project.worktrees().next().unwrap(); + let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap(); + project.expand_entry(worktree_id, entry.id, cx).unwrap() + }) + .await + .unwrap(); + + project_b.read_with(cx_b, |project, cx| { + let worktree = project.worktrees().next().unwrap().read(cx); + assert_eq!( + worktree.paths().map(AsRef::as_ref).collect::>(), + [ + Path::new(".gitignore"), + Path::new("a.txt"), + Path::new("b.txt"), + Path::new("ignored-dir"), + Path::new("ignored-dir/c.txt"), + Path::new("ignored-dir/d.txt"), + ] + ); + }); + + // Open the same file as client B and client A. + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) + .await + .unwrap(); + + buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); + + project_a.read_with(cx_a, |project, cx| { + assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) + }); + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) + .await + .unwrap(); + + let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); + + // Client A sees client B's selection + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + buffer + .snapshot() + .remote_selections_in_range(Anchor::MIN..Anchor::MAX) + .count() + == 1 + }); + + // Edit the buffer as client B and see that edit as client A. + editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx)); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "ok, b-contents") + }); + + // Client B can invite client C on a project shared by client A. + active_call_b + .update(cx_b, |call, cx| { + call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx) + }) + .await + .unwrap(); + + let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); + executor.run_until_parked(); + let call = incoming_call_c.borrow().clone().unwrap(); + assert_eq!(call.calling_user.github_login, "user_b"); + let initial_project = call.initial_project.unwrap(); + active_call_c + .update(cx_c, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + let _project_c = client_c + .build_remote_project(initial_project.id, cx_c) + .await; + + // Client B closes the editor, and client A sees client B's selections removed. + cx_b.update(move |_| drop(editor_b)); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + buffer + .snapshot() + .remote_selections_in_range(Anchor::MIN..Anchor::MAX) + .count() + == 0 + }); +} diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 5b5a40ba8e..01b8d24801 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -1,8191 +1,8191 @@ -// use super::*; -// use crate::{ -// scroll::scroll_amount::ScrollAmount, -// test::{ -// assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, -// editor_test_context::EditorTestContext, select_ranges, -// }, -// JoinLines, -// }; -// use drag_and_drop::DragAndDrop; -// use futures::StreamExt; -// use gpui::{ -// executor::Deterministic, -// geometry::{rect::RectF, vector::vec2f}, -// platform::{WindowBounds, WindowOptions}, -// serde_json::{self, json}, -// TestAppContext, -// }; -// use indoc::indoc; -// use language::{ -// language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, -// BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, -// Override, Point, -// }; -// use parking_lot::Mutex; -// use project::project_settings::{LspSettings, ProjectSettings}; -// use project::FakeFs; -// use std::sync::atomic; -// use std::sync::atomic::AtomicUsize; -// use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; -// use unindent::Unindent; -// use util::{ -// assert_set_eq, -// test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, -// }; -// use workspace::{ -// item::{FollowableItem, Item, ItemHandle}, -// NavigationEntry, ViewId, -// }; - -// #[gpui::test] -// fn test_edit_events(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let buffer = cx.add_model(|cx| { -// let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456"); -// buffer.set_group_interval(Duration::from_secs(1)); -// buffer -// }); - -// let events = Rc::new(RefCell::new(Vec::new())); -// let editor1 = cx -// .add_window({ -// let events = events.clone(); -// |cx| { -// cx.subscribe(&cx.handle(), move |_, _, event, _| { -// if matches!( -// event, -// Event::Edited | Event::BufferEdited | Event::DirtyChanged -// ) { -// events.borrow_mut().push(("editor1", event.clone())); -// } -// }) -// .detach(); -// Editor::for_buffer(buffer.clone(), None, cx) -// } -// }) -// .root(cx); -// let editor2 = cx -// .add_window({ -// let events = events.clone(); -// |cx| { -// cx.subscribe(&cx.handle(), move |_, _, event, _| { -// if matches!( -// event, -// Event::Edited | Event::BufferEdited | Event::DirtyChanged -// ) { -// events.borrow_mut().push(("editor2", event.clone())); -// } -// }) -// .detach(); -// Editor::for_buffer(buffer.clone(), None, cx) -// } -// }) -// .root(cx); -// assert_eq!(mem::take(&mut *events.borrow_mut()), []); - -// // Mutating editor 1 will emit an `Edited` event only for that editor. -// editor1.update(cx, |editor, cx| editor.insert("X", cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor1", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ("editor1", Event::DirtyChanged), -// ("editor2", Event::DirtyChanged) -// ] -// ); - -// // Mutating editor 2 will emit an `Edited` event only for that editor. -// editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor2", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ] -// ); - -// // Undoing on editor 1 will emit an `Edited` event only for that editor. -// editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor1", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ("editor1", Event::DirtyChanged), -// ("editor2", Event::DirtyChanged), -// ] -// ); - -// // Redoing on editor 1 will emit an `Edited` event only for that editor. -// editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor1", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ("editor1", Event::DirtyChanged), -// ("editor2", Event::DirtyChanged), -// ] -// ); - -// // Undoing on editor 2 will emit an `Edited` event only for that editor. -// editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor2", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ("editor1", Event::DirtyChanged), -// ("editor2", Event::DirtyChanged), -// ] -// ); - -// // Redoing on editor 2 will emit an `Edited` event only for that editor. -// editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor2", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ("editor1", Event::DirtyChanged), -// ("editor2", Event::DirtyChanged), -// ] -// ); - -// // No event is emitted when the mutation is a no-op. -// editor2.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([0..0])); - -// editor.backspace(&Backspace, cx); -// }); -// assert_eq!(mem::take(&mut *events.borrow_mut()), []); -// } - -// #[gpui::test] -// fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let mut now = Instant::now(); -// let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456")); -// let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx -// .add_window(|cx| build_editor(buffer.clone(), cx)) -// .root(cx); - -// editor.update(cx, |editor, cx| { -// editor.start_transaction_at(now, cx); -// editor.change_selections(None, cx, |s| s.select_ranges([2..4])); - -// editor.insert("cd", cx); -// editor.end_transaction_at(now, cx); -// assert_eq!(editor.text(cx), "12cd56"); -// assert_eq!(editor.selections.ranges(cx), vec![4..4]); - -// editor.start_transaction_at(now, cx); -// editor.change_selections(None, cx, |s| s.select_ranges([4..5])); -// editor.insert("e", cx); -// editor.end_transaction_at(now, cx); -// assert_eq!(editor.text(cx), "12cde6"); -// assert_eq!(editor.selections.ranges(cx), vec![5..5]); - -// now += group_interval + Duration::from_millis(1); -// editor.change_selections(None, cx, |s| s.select_ranges([2..2])); - -// // Simulate an edit in another editor -// buffer.update(cx, |buffer, cx| { -// buffer.start_transaction_at(now, cx); -// buffer.edit([(0..1, "a")], None, cx); -// buffer.edit([(1..1, "b")], None, cx); -// buffer.end_transaction_at(now, cx); -// }); - -// assert_eq!(editor.text(cx), "ab2cde6"); -// assert_eq!(editor.selections.ranges(cx), vec![3..3]); - -// // Last transaction happened past the group interval in a different editor. -// // Undo it individually and don't restore selections. -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "12cde6"); -// assert_eq!(editor.selections.ranges(cx), vec![2..2]); - -// // First two transactions happened within the group interval in this editor. -// // Undo them together and restore selections. -// editor.undo(&Undo, cx); -// editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. -// assert_eq!(editor.text(cx), "123456"); -// assert_eq!(editor.selections.ranges(cx), vec![0..0]); - -// // Redo the first two transactions together. -// editor.redo(&Redo, cx); -// assert_eq!(editor.text(cx), "12cde6"); -// assert_eq!(editor.selections.ranges(cx), vec![5..5]); - -// // Redo the last transaction on its own. -// editor.redo(&Redo, cx); -// assert_eq!(editor.text(cx), "ab2cde6"); -// assert_eq!(editor.selections.ranges(cx), vec![6..6]); - -// // Test empty transactions. -// editor.start_transaction_at(now, cx); -// editor.end_transaction_at(now, cx); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "12cde6"); -// }); -// } - -// #[gpui::test] -// fn test_ime_composition(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let buffer = cx.add_model(|cx| { -// let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde"); -// // Ensure automatic grouping doesn't occur. -// buffer.set_group_interval(Duration::ZERO); -// buffer -// }); - -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// cx.add_window(|cx| { -// let mut editor = build_editor(buffer.clone(), cx); - -// // Start a new IME composition. -// editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); -// editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); -// editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); -// assert_eq!(editor.text(cx), "äbcde"); -// assert_eq!( -// editor.marked_text_ranges(cx), -// Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) -// ); - -// // Finalize IME composition. -// editor.replace_text_in_range(None, "ā", cx); -// assert_eq!(editor.text(cx), "ābcde"); -// assert_eq!(editor.marked_text_ranges(cx), None); - -// // IME composition edits are grouped and are undone/redone at once. -// editor.undo(&Default::default(), cx); -// assert_eq!(editor.text(cx), "abcde"); -// assert_eq!(editor.marked_text_ranges(cx), None); -// editor.redo(&Default::default(), cx); -// assert_eq!(editor.text(cx), "ābcde"); -// assert_eq!(editor.marked_text_ranges(cx), None); - -// // Start a new IME composition. -// editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); -// assert_eq!( -// editor.marked_text_ranges(cx), -// Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) -// ); - -// // Undoing during an IME composition cancels it. -// editor.undo(&Default::default(), cx); -// assert_eq!(editor.text(cx), "ābcde"); -// assert_eq!(editor.marked_text_ranges(cx), None); - -// // Start a new IME composition with an invalid marked range, ensuring it gets clipped. -// editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); -// assert_eq!(editor.text(cx), "ābcdè"); -// assert_eq!( -// editor.marked_text_ranges(cx), -// Some(vec![OffsetUtf16(4)..OffsetUtf16(5)]) -// ); - -// // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. -// editor.replace_text_in_range(Some(4..999), "ę", cx); -// assert_eq!(editor.text(cx), "ābcdę"); -// assert_eq!(editor.marked_text_ranges(cx), None); - -// // Start a new IME composition with multiple cursors. -// editor.change_selections(None, cx, |s| { -// s.select_ranges([ -// OffsetUtf16(1)..OffsetUtf16(1), -// OffsetUtf16(3)..OffsetUtf16(3), -// OffsetUtf16(5)..OffsetUtf16(5), -// ]) -// }); -// editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); -// assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); -// assert_eq!( -// editor.marked_text_ranges(cx), -// Some(vec![ -// OffsetUtf16(0)..OffsetUtf16(3), -// OffsetUtf16(4)..OffsetUtf16(7), -// OffsetUtf16(8)..OffsetUtf16(11) -// ]) -// ); - -// // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. -// editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); -// assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); -// assert_eq!( -// editor.marked_text_ranges(cx), -// Some(vec![ -// OffsetUtf16(1)..OffsetUtf16(2), -// OffsetUtf16(5)..OffsetUtf16(6), -// OffsetUtf16(9)..OffsetUtf16(10) -// ]) -// ); - -// // Finalize IME composition with multiple cursors. -// editor.replace_text_in_range(Some(9..10), "2", cx); -// assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); -// assert_eq!(editor.marked_text_ranges(cx), None); - -// editor -// }); -// } - -// #[gpui::test] -// fn test_selection_with_mouse(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// editor.update(cx, |view, cx| { -// view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); -// }); -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] -// ); - -// editor.update(cx, |view, cx| { -// view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); -// }); - -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] -// ); - -// editor.update(cx, |view, cx| { -// view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); -// }); - -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] -// ); - -// editor.update(cx, |view, cx| { -// view.end_selection(cx); -// view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); -// }); - -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] -// ); - -// editor.update(cx, |view, cx| { -// view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); -// view.update_selection(DisplayPoint::new(0, 0), 0, Point::zero(), cx); -// }); - -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [ -// DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), -// DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) -// ] -// ); - -// editor.update(cx, |view, cx| { -// view.end_selection(cx); -// }); - -// assert_eq!( -// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), -// [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] -// ); -// } - -// #[gpui::test] -// fn test_canceling_pending_selection(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.cancel(&Cancel, cx); -// view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] -// ); -// }); -// } - -// #[gpui::test] -// fn test_clone(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let (text, selection_ranges) = marked_text_ranges( -// indoc! {" -// one -// two -// threeˇ -// four -// fiveˇ -// "}, -// true, -// ); - -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&text, cx); -// build_editor(buffer, cx) -// }) -// .root(cx); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); -// editor.fold_ranges( -// [ -// Point::new(1, 0)..Point::new(2, 0), -// Point::new(3, 0)..Point::new(4, 0), -// ], -// true, -// cx, -// ); -// }); - -// let cloned_editor = editor -// .update(cx, |editor, cx| { -// cx.add_window(Default::default(), |cx| editor.clone(cx)) -// }) -// .root(cx); - -// let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); -// let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); - -// assert_eq!( -// cloned_editor.update(cx, |e, cx| e.display_text(cx)), -// editor.update(cx, |e, cx| e.display_text(cx)) -// ); -// assert_eq!( -// cloned_snapshot -// .folds_in_range(0..text.len()) -// .collect::>(), -// 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)) -// ); -// } - -// #[gpui::test] -// async fn test_navigation_history(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// cx.set_global(DragAndDrop::::default()); -// use workspace::item::Item; - -// let fs = FakeFs::new(cx.background()); -// let project = Project::test(fs, [], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); -// window.add_view(cx, |cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); -// let mut editor = build_editor(buffer.clone(), cx); -// let handle = cx.handle(); -// editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); - -// fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { -// editor.nav_history.as_mut().unwrap().pop_backward(cx) -// } - -// // Move the cursor a small distance. -// // Nothing is added to the navigation history. -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) -// }); -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) -// }); -// assert!(pop_history(&mut editor, cx).is_none()); - -// // Move the cursor a large distance. -// // The history can jump back to the previous position. -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)]) -// }); -// let nav_entry = pop_history(&mut editor, cx).unwrap(); -// editor.navigate(nav_entry.data.unwrap(), cx); -// assert_eq!(nav_entry.item.id(), cx.view_id()); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] -// ); -// assert!(pop_history(&mut editor, cx).is_none()); - -// // Move the cursor a small distance via the mouse. -// // Nothing is added to the navigation history. -// editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); -// editor.end_selection(cx); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] -// ); -// assert!(pop_history(&mut editor, cx).is_none()); - -// // Move the cursor a large distance via the mouse. -// // The history can jump back to the previous position. -// editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); -// editor.end_selection(cx); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] -// ); -// let nav_entry = pop_history(&mut editor, cx).unwrap(); -// editor.navigate(nav_entry.data.unwrap(), cx); -// assert_eq!(nav_entry.item.id(), cx.view_id()); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] -// ); -// assert!(pop_history(&mut editor, cx).is_none()); - -// // Set scroll position to check later -// editor.set_scroll_position(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)] -// ); -// assert_eq!( -// editor.scroll_position(cx), -// vec2f(0., editor.max_point(cx).row() as f32) -// ); - -// editor -// }); -// } - -// #[gpui::test] -// fn test_cancel(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); -// view.update_selection(DisplayPoint::new(1, 1), 0, 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); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), -// DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.cancel(&Cancel, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.cancel(&Cancel, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] -// ); -// }); -// } - -// #[gpui::test] -// fn test_fold_action(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple( -// &" -// impl Foo { -// // Hello! - -// fn a() { -// 1 -// } - -// fn b() { -// 2 -// } - -// fn c() { -// 3 -// } -// } -// " -// .unindent(), -// cx, -// ); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]); -// }); -// view.fold(&Fold, cx); -// assert_eq!( -// view.display_text(cx), -// " -// impl Foo { -// // Hello! - -// fn a() { -// 1 -// } - -// fn b() {⋯ -// } - -// fn c() {⋯ -// } -// } -// " -// .unindent(), -// ); - -// view.fold(&Fold, cx); -// assert_eq!( -// view.display_text(cx), -// " -// impl Foo {⋯ -// } -// " -// .unindent(), -// ); - -// view.unfold_lines(&UnfoldLines, cx); -// assert_eq!( -// view.display_text(cx), -// " -// impl Foo { -// // Hello! - -// fn a() { -// 1 -// } - -// fn b() {⋯ -// } - -// fn c() {⋯ -// } -// } -// " -// .unindent(), -// ); - -// view.unfold_lines(&UnfoldLines, cx); -// assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); -// }); -// } - -// #[gpui::test] -// fn test_move_cursor(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); -// let view = cx -// .add_window(|cx| build_editor(buffer.clone(), cx)) -// .root(cx); - -// buffer.update(cx, |buffer, cx| { -// buffer.edit( -// vec![ -// (Point::new(1, 0)..Point::new(1, 0), "\t"), -// (Point::new(1, 1)..Point::new(1, 1), "\t"), -// ], -// None, -// cx, -// ); -// }); -// view.update(cx, |view, cx| { -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] -// ); - -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] -// ); - -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] -// ); - -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] -// ); - -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] -// ); - -// view.move_to_end(&MoveToEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] -// ); - -// view.move_to_beginning(&MoveToBeginning, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] -// ); - -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]); -// }); -// view.select_to_beginning(&SelectToBeginning, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] -// ); - -// view.select_to_end(&SelectToEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] -// ); -// }); -// } - -// #[gpui::test] -// fn test_move_cursor_multibyte(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); - -// assert_eq!('ⓐ'.len_utf8(), 3); -// assert_eq!('α'.len_utf8(), 2); - -// view.update(cx, |view, cx| { -// view.fold_ranges( -// vec![ -// Point::new(0, 6)..Point::new(0, 12), -// Point::new(1, 2)..Point::new(1, 4), -// Point::new(2, 4)..Point::new(2, 8), -// ], -// true, -// cx, -// ); -// assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε"); - -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "ⓐ".len())] -// ); -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "ⓐⓑ".len())] -// ); -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "ⓐⓑ⋯".len())] -// ); - -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "ab⋯e".len())] -// ); -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "ab⋯".len())] -// ); -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "ab".len())] -// ); -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "a".len())] -// ); - -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "α".len())] -// ); -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβ".len())] -// ); -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβ⋯".len())] -// ); -// view.move_right(&MoveRight, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβ⋯ε".len())] -// ); - -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "ab⋯e".len())] -// ); -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβ⋯ε".len())] -// ); -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "ab⋯e".len())] -// ); - -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "ⓐⓑ".len())] -// ); -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "ⓐ".len())] -// ); -// view.move_left(&MoveLeft, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(0, "".len())] -// ); -// }); -// } - -// #[gpui::test] -// fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); -// }); -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "abcd".len())] -// ); - -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβγ".len())] -// ); - -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(3, "abcd".len())] -// ); - -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] -// ); - -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(3, "abcd".len())] -// ); - -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβγ".len())] -// ); -// }); -// } - -// #[gpui::test] -// fn test_beginning_end_of_line(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\n def", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), -// ]); -// }); -// }); - -// view.update(cx, |view, cx| { -// view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.move_to_end_of_line(&MoveToEndOfLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), -// ] -// ); -// }); - -// // Moving to the end of line again is a no-op. -// view.update(cx, |view, cx| { -// view.move_to_end_of_line(&MoveToEndOfLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.move_left(&MoveLeft, cx); -// view.select_to_beginning_of_line( -// &SelectToBeginningOfLine { -// stop_at_soft_wraps: true, -// }, -// cx, -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.select_to_beginning_of_line( -// &SelectToBeginningOfLine { -// stop_at_soft_wraps: true, -// }, -// cx, -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.select_to_beginning_of_line( -// &SelectToBeginningOfLine { -// stop_at_soft_wraps: true, -// }, -// cx, -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.select_to_end_of_line( -// &SelectToEndOfLine { -// stop_at_soft_wraps: true, -// }, -// cx, -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.delete_to_end_of_line(&DeleteToEndOfLine, cx); -// assert_eq!(view.display_text(cx), "ab\n de"); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); -// assert_eq!(view.display_text(cx), "\n"); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), -// ] -// ); -// }); -// } - -// #[gpui::test] -// fn test_prev_next_word_boundary(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), -// DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), -// ]) -// }); - -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); - -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx); - -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx); - -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); - -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx); - -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx); - -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); - -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); - -// view.move_right(&MoveRight, cx); -// view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); -// assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); - -// view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); -// assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx); - -// view.select_to_next_word_end(&SelectToNextWordEnd, cx); -// assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); -// }); -// } - -// #[gpui::test] -// fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = -// MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.set_wrap_width(Some(140.), cx); -// assert_eq!( -// view.display_text(cx), -// "use one::{\n two::three::\n four::five\n};" -// ); - -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]); -// }); - -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] -// ); - -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] -// ); - -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] -// ); - -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] -// ); - -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] -// ); - -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] -// ); -// }); -// } - -// #[gpui::test] -// async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; - -// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); -// let window = cx.window; -// window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); - -// cx.set_state( -// &r#"ˇone -// two - -// three -// fourˇ -// five - -// six"# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two -// ˇ -// three -// four -// five -// ˇ -// six"# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two - -// three -// four -// five -// ˇ -// sixˇ"# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two - -// three -// four -// five - -// sixˇ"# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two - -// three -// four -// five -// ˇ -// six"# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two -// ˇ -// three -// four -// five - -// six"# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"ˇone -// two - -// three -// four -// five - -// six"# -// .unindent(), -// ); -// } - -// #[gpui::test] -// async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; -// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); -// let window = cx.window; -// window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx); - -// cx.set_state( -// &r#"ˇone -// two -// three -// four -// five -// six -// seven -// eight -// nine -// ten -// "#, -// ); - -// cx.update_editor(|editor, cx| { -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)); -// editor.scroll_screen(&ScrollAmount::Page(1.), cx); -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); -// editor.scroll_screen(&ScrollAmount::Page(1.), cx); -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.)); -// editor.scroll_screen(&ScrollAmount::Page(-1.), cx); -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); - -// editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.)); -// editor.scroll_screen(&ScrollAmount::Page(0.5), cx); -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); -// }); -// } - -// #[gpui::test] -// async fn test_autoscroll(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; - -// let line_height = cx.update_editor(|editor, cx| { -// editor.set_vertical_scroll_margin(2, cx); -// editor.style(cx).text.line_height(cx.font_cache()) -// }); - -// let window = cx.window; -// window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx); - -// cx.set_state( -// &r#"ˇone -// two -// three -// four -// five -// six -// seven -// eight -// nine -// ten -// "#, -// ); -// cx.update_editor(|editor, cx| { -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0)); -// }); - -// // Add a cursor below the visible area. Since both cursors cannot fit -// // on screen, the editor autoscrolls to reveal the newest cursor, and -// // allows the vertical scroll margin below that cursor. -// cx.update_editor(|editor, cx| { -// editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { -// selections.select_ranges([ -// Point::new(0, 0)..Point::new(0, 0), -// Point::new(6, 0)..Point::new(6, 0), -// ]); -// }) -// }); -// cx.update_editor(|editor, cx| { -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0)); -// }); - -// // Move down. The editor cursor scrolls down to track the newest cursor. -// cx.update_editor(|editor, cx| { -// editor.move_down(&Default::default(), cx); -// }); -// cx.update_editor(|editor, cx| { -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0)); -// }); - -// // Add a cursor above the visible area. Since both cursors fit on screen, -// // the editor scrolls to show both. -// cx.update_editor(|editor, cx| { -// editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { -// selections.select_ranges([ -// Point::new(1, 0)..Point::new(1, 0), -// Point::new(6, 0)..Point::new(6, 0), -// ]); -// }) -// }); -// cx.update_editor(|editor, cx| { -// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0)); -// }); -// } - -// #[gpui::test] -// async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; - -// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); -// let window = cx.window; -// window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); - -// cx.set_state( -// &r#" -// ˇone -// two -// threeˇ -// four -// five -// six -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// ˇfour -// five -// sixˇ -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// four -// five -// six -// ˇseven -// eight -// nineˇ -// ten -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// ˇfour -// five -// sixˇ -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); -// cx.assert_editor_state( -// &r#" -// ˇone -// two -// threeˇ -// four -// five -// six -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); - -// // Test select collapsing -// cx.update_editor(|editor, cx| { -// editor.move_page_down(&MovePageDown::default(), cx); -// editor.move_page_down(&MovePageDown::default(), cx); -// editor.move_page_down(&MovePageDown::default(), cx); -// }); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// four -// five -// six -// seven -// eight -// nine -// ˇten -// ˇ"# -// .unindent(), -// ); -// } - -// #[gpui::test] -// async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state("one «two threeˇ» four"); -// cx.update_editor(|editor, cx| { -// editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); -// assert_eq!(editor.text(cx), " four"); -// }); -// } - -// #[gpui::test] -// fn test_delete_to_word_boundary(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("one two three four", cx); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// // an empty selection - the preceding word fragment is deleted -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// // characters selected - they are deleted -// DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12), -// ]) -// }); -// view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx); -// assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four"); -// }); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// // an empty selection - the following word fragment is deleted -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// // characters selected - they are deleted -// DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10), -// ]) -// }); -// view.delete_to_next_word_end(&DeleteToNextWordEnd, cx); -// assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our"); -// }); -// } - -// #[gpui::test] -// fn test_newline(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), -// DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), -// ]) -// }); - -// view.newline(&Newline, cx); -// assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); -// }); -// } - -// #[gpui::test] -// fn test_newline_with_old_selections(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple( -// " -// a -// b( -// X -// ) -// c( -// X -// ) -// " -// .unindent() -// .as_str(), -// cx, -// ); -// let mut editor = build_editor(buffer.clone(), cx); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([ -// Point::new(2, 4)..Point::new(2, 5), -// Point::new(5, 4)..Point::new(5, 5), -// ]) -// }); -// editor -// }) -// .root(cx); - -// editor.update(cx, |editor, cx| { -// // Edit the buffer directly, deleting ranges surrounding the editor's selections -// editor.buffer.update(cx, |buffer, cx| { -// buffer.edit( -// [ -// (Point::new(1, 2)..Point::new(3, 0), ""), -// (Point::new(4, 2)..Point::new(6, 0), ""), -// ], -// None, -// cx, -// ); -// assert_eq!( -// buffer.read(cx).text(), -// " -// a -// b() -// c() -// " -// .unindent() -// ); -// }); -// assert_eq!( -// editor.selections.ranges(cx), -// &[ -// Point::new(1, 2)..Point::new(1, 2), -// Point::new(2, 2)..Point::new(2, 2), -// ], -// ); - -// editor.newline(&Newline, cx); -// assert_eq!( -// editor.text(cx), -// " -// a -// b( -// ) -// c( -// ) -// " -// .unindent() -// ); - -// // The selections are moved after the inserted newlines -// assert_eq!( -// editor.selections.ranges(cx), -// &[ -// Point::new(2, 0)..Point::new(2, 0), -// Point::new(4, 0)..Point::new(4, 0), -// ], -// ); -// }); -// } - -// #[gpui::test] -// async fn test_newline_above(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(4) -// }); - -// let language = Arc::new( -// Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query(r#"(_ "(" ")" @end) @indent"#) -// .unwrap(), -// ); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); -// cx.set_state(indoc! {" -// const a: ˇA = ( -// (ˇ -// «const_functionˇ»(ˇ), -// so«mˇ»et«hˇ»ing_ˇelse,ˇ -// )ˇ -// ˇ);ˇ -// "}); - -// cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); -// cx.assert_editor_state(indoc! {" -// ˇ -// const a: A = ( -// ˇ -// ( -// ˇ -// ˇ -// const_function(), -// ˇ -// ˇ -// ˇ -// ˇ -// something_else, -// ˇ -// ) -// ˇ -// ˇ -// ); -// "}); -// } - -// #[gpui::test] -// async fn test_newline_below(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(4) -// }); - -// let language = Arc::new( -// Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query(r#"(_ "(" ")" @end) @indent"#) -// .unwrap(), -// ); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); -// cx.set_state(indoc! {" -// const a: ˇA = ( -// (ˇ -// «const_functionˇ»(ˇ), -// so«mˇ»et«hˇ»ing_ˇelse,ˇ -// )ˇ -// ˇ);ˇ -// "}); - -// cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); -// cx.assert_editor_state(indoc! {" -// const a: A = ( -// ˇ -// ( -// ˇ -// const_function(), -// ˇ -// ˇ -// something_else, -// ˇ -// ˇ -// ˇ -// ˇ -// ) -// ˇ -// ); -// ˇ -// ˇ -// "}); -// } - -// #[gpui::test] -// async fn test_newline_comments(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(4) -// }); - -// let language = Arc::new(Language::new( -// LanguageConfig { -// line_comment: Some("//".into()), -// ..LanguageConfig::default() -// }, -// None, -// )); -// { -// let mut cx = EditorTestContext::new(cx).await; -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); -// cx.set_state(indoc! {" -// // Fooˇ -// "}); - -// cx.update_editor(|e, cx| e.newline(&Newline, cx)); -// cx.assert_editor_state(indoc! {" -// // Foo -// //ˇ -// "}); -// // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix. -// cx.set_state(indoc! {" -// ˇ// Foo -// "}); -// cx.update_editor(|e, cx| e.newline(&Newline, cx)); -// cx.assert_editor_state(indoc! {" - -// ˇ// Foo -// "}); -// } -// // Ensure that comment continuations can be disabled. -// update_test_language_settings(cx, |settings| { -// settings.defaults.extend_comment_on_newline = Some(false); -// }); -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state(indoc! {" -// // Fooˇ -// "}); -// cx.update_editor(|e, cx| e.newline(&Newline, cx)); -// cx.assert_editor_state(indoc! {" -// // Foo -// ˇ -// "}); -// } - -// #[gpui::test] -// fn test_insert_with_old_selections(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); -// let mut editor = build_editor(buffer.clone(), cx); -// editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); -// editor -// }) -// .root(cx); - -// editor.update(cx, |editor, cx| { -// // Edit the buffer directly, deleting ranges surrounding the editor's selections -// editor.buffer.update(cx, |buffer, cx| { -// buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); -// assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); -// }); -// assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],); - -// editor.insert("Z", cx); -// assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); - -// // The selections are moved after the inserted characters -// assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],); -// }); -// } - -// #[gpui::test] -// async fn test_tab(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(3) -// }); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state(indoc! {" -// ˇabˇc -// ˇ🏀ˇ🏀ˇefg -// dˇ -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// ˇab ˇc -// ˇ🏀 ˇ🏀 ˇefg -// d ˇ -// "}); - -// cx.set_state(indoc! {" -// a -// «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// a -// «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» -// "}); -// } - -// #[gpui::test] -// async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; -// let language = Arc::new( -// Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query(r#"(_ "(" ")" @end) @indent"#) -// .unwrap(), -// ); -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - -// // cursors that are already at the suggested indent level insert -// // a soft tab. cursors that are to the left of the suggested indent -// // auto-indent their line. -// cx.set_state(indoc! {" -// ˇ -// const a: B = ( -// c( -// d( -// ˇ -// ) -// ˇ -// ˇ ) -// ); -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// ˇ -// const a: B = ( -// c( -// d( -// ˇ -// ) -// ˇ -// ˇ) -// ); -// "}); - -// // handle auto-indent when there are multiple cursors on the same line -// cx.set_state(indoc! {" -// const a: B = ( -// c( -// ˇ ˇ -// ˇ ) -// ); -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c( -// ˇ -// ˇ) -// ); -// "}); -// } - -// #[gpui::test] -// async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(4) -// }); - -// let language = Arc::new( -// Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query(r#"(_ "{" "}" @end) @indent"#) -// .unwrap(), -// ); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); -// cx.set_state(indoc! {" -// fn a() { -// if b { -// \t ˇc -// } -// } -// "}); - -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// fn a() { -// if b { -// ˇc -// } -// } -// "}); -// } - -// #[gpui::test] -// async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.tab_size = NonZeroU32::new(4); -// }); - -// let mut cx = EditorTestContext::new(cx).await; - -// cx.set_state(indoc! {" -// «oneˇ» «twoˇ» -// three -// four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// «oneˇ» «twoˇ» -// three -// four -// "}); - -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// «oneˇ» «twoˇ» -// three -// four -// "}); - -// // select across line ending -// cx.set_state(indoc! {" -// one two -// t«hree -// ˇ» four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// t«hree -// ˇ» four -// "}); - -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// t«hree -// ˇ» four -// "}); - -// // Ensure that indenting/outdenting works when the cursor is at column 0. -// cx.set_state(indoc! {" -// one two -// ˇthree -// four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// ˇthree -// four -// "}); - -// cx.set_state(indoc! {" -// one two -// ˇ three -// four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// ˇthree -// four -// "}); -// } - -// #[gpui::test] -// async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.hard_tabs = Some(true); -// }); - -// let mut cx = EditorTestContext::new(cx).await; - -// // select two ranges on one line -// cx.set_state(indoc! {" -// «oneˇ» «twoˇ» -// three -// four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// \t«oneˇ» «twoˇ» -// three -// four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// \t\t«oneˇ» «twoˇ» -// three -// four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// \t«oneˇ» «twoˇ» -// three -// four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// «oneˇ» «twoˇ» -// three -// four -// "}); - -// // select across a line ending -// cx.set_state(indoc! {" -// one two -// t«hree -// ˇ»four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// \tt«hree -// ˇ»four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// \t\tt«hree -// ˇ»four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// \tt«hree -// ˇ»four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// t«hree -// ˇ»four -// "}); - -// // Ensure that indenting/outdenting works when the cursor is at column 0. -// cx.set_state(indoc! {" -// one two -// ˇthree -// four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// ˇthree -// four -// "}); -// cx.update_editor(|e, cx| e.tab(&Tab, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// \tˇthree -// four -// "}); -// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); -// cx.assert_editor_state(indoc! {" -// one two -// ˇthree -// four -// "}); -// } - -// #[gpui::test] -// fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { -// init_test(cx, |settings| { -// settings.languages.extend([ -// ( -// "TOML".into(), -// LanguageSettingsContent { -// tab_size: NonZeroU32::new(2), -// ..Default::default() -// }, -// ), -// ( -// "Rust".into(), -// LanguageSettingsContent { -// tab_size: NonZeroU32::new(4), -// ..Default::default() -// }, -// ), -// ]); -// }); - -// let toml_language = Arc::new(Language::new( -// LanguageConfig { -// name: "TOML".into(), -// ..Default::default() -// }, -// None, -// )); -// let rust_language = Arc::new(Language::new( -// LanguageConfig { -// name: "Rust".into(), -// ..Default::default() -// }, -// None, -// )); - -// let toml_buffer = cx.add_model(|cx| { -// Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx) -// }); -// let rust_buffer = cx.add_model(|cx| { -// Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") -// .with_language(rust_language, cx) -// }); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// toml_buffer.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }], -// cx, -// ); -// multibuffer.push_excerpts( -// rust_buffer.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 0), -// primary: None, -// }], -// cx, -// ); -// multibuffer -// }); - -// cx.add_window(|cx| { -// let mut editor = build_editor(multibuffer, cx); - -// assert_eq!( -// editor.text(cx), -// indoc! {" -// a = 1 -// b = 2 - -// const c: usize = 3; -// "} -// ); - -// select_ranges( -// &mut editor, -// indoc! {" -// «aˇ» = 1 -// b = 2 - -// «const c:ˇ» usize = 3; -// "}, -// cx, -// ); - -// editor.tab(&Tab, cx); -// assert_text_with_selections( -// &mut editor, -// indoc! {" -// «aˇ» = 1 -// b = 2 - -// «const c:ˇ» usize = 3; -// "}, -// cx, -// ); -// editor.tab_prev(&TabPrev, cx); -// assert_text_with_selections( -// &mut editor, -// indoc! {" -// «aˇ» = 1 -// b = 2 - -// «const c:ˇ» usize = 3; -// "}, -// cx, -// ); - -// editor -// }); -// } - -// #[gpui::test] -// async fn test_backspace(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// // Basic backspace -// cx.set_state(indoc! {" -// onˇe two three -// fou«rˇ» five six -// seven «ˇeight nine -// »ten -// "}); -// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); -// cx.assert_editor_state(indoc! {" -// oˇe two three -// fouˇ five six -// seven ˇten -// "}); - -// // Test backspace inside and around indents -// cx.set_state(indoc! {" -// zero -// ˇone -// ˇtwo -// ˇ ˇ ˇ three -// ˇ ˇ four -// "}); -// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); -// cx.assert_editor_state(indoc! {" -// zero -// ˇone -// ˇtwo -// ˇ threeˇ four -// "}); - -// // Test backspace with line_mode set to true -// cx.update_editor(|e, _| e.selections.line_mode = true); -// cx.set_state(indoc! {" -// The ˇquick ˇbrown -// fox jumps over -// the lazy dog -// ˇThe qu«ick bˇ»rown"}); -// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); -// cx.assert_editor_state(indoc! {" -// ˇfox jumps over -// the lazy dogˇ"}); -// } - -// #[gpui::test] -// async fn test_delete(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state(indoc! {" -// onˇe two three -// fou«rˇ» five six -// seven «ˇeight nine -// »ten -// "}); -// cx.update_editor(|e, cx| e.delete(&Delete, cx)); -// cx.assert_editor_state(indoc! {" -// onˇ two three -// fouˇ five six -// seven ˇten -// "}); - -// // Test backspace with line_mode set to true -// cx.update_editor(|e, _| e.selections.line_mode = true); -// cx.set_state(indoc! {" -// The ˇquick ˇbrown -// fox «ˇjum»ps over -// the lazy dog -// ˇThe qu«ick bˇ»rown"}); -// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); -// cx.assert_editor_state("ˇthe lazy dogˇ"); -// } - -// #[gpui::test] -// fn test_delete_line(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), -// DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), -// ]) -// }); -// view.delete_line(&DeleteLine, cx); -// assert_eq!(view.display_text(cx), "ghi"); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) -// ] -// ); -// }); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) -// }); -// view.delete_line(&DeleteLine, cx); -// assert_eq!(view.display_text(cx), "ghi\n"); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] -// ); -// }); -// } - -// #[gpui::test] -// fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// cx.add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); -// let mut editor = build_editor(buffer.clone(), cx); -// let buffer = buffer.read(cx).as_singleton().unwrap(); - -// assert_eq!( -// editor.selections.ranges::(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), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.select_line(&SelectLine, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] -// ); -// }); -// } - -// #[gpui::test] -// fn test_split_selection_into_lines(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); -// build_editor(buffer, cx) -// }) -// .root(cx); -// view.update(cx, |view, cx| { -// view.fold_ranges( -// vec![ -// Point::new(0, 2)..Point::new(1, 2), -// Point::new(2, 3)..Point::new(4, 1), -// Point::new(7, 0)..Point::new(8, 4), -// ], -// true, -// cx, -// ); -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), -// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), -// ]) -// }); -// assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"); -// }); - -// view.update(cx, |view, cx| { -// view.split_selection_into_lines(&SplitSelectionIntoLines, cx); -// assert_eq!( -// view.display_text(cx), -// "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), -// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), -// DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)]) -// }); -// view.split_selection_into_lines(&SplitSelectionIntoLines, cx); -// assert_eq!( -// view.display_text(cx), -// "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), -// DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), -// DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), -// DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), -// DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), -// DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), -// DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), -// DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) -// ] -// ); -// }); -// } - -// #[gpui::test] -// fn test_add_selection_above_below(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let view = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); -// build_editor(buffer, cx) -// }) -// .root(cx); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]) -// }); -// }); -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] -// ); - -// view.undo_selection(&UndoSelection, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) -// ] -// ); - -// view.redo_selection(&RedoSelection, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), -// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), -// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]) -// }); -// }); -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), -// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), -// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)]) -// }); -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), -// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), -// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), -// DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), -// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)]) -// }); -// }); -// view.update(cx, |view, cx| { -// view.add_selection_above(&AddSelectionAbove, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), -// DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), -// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), -// ] -// ); -// }); - -// view.update(cx, |view, cx| { -// view.add_selection_below(&AddSelectionBelow, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// vec![ -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), -// DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), -// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), -// ] -// ); -// }); -// } - -// #[gpui::test] -// async fn test_select_next(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); - -// cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); -// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); -// cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); - -// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); -// } - -// #[gpui::test] -// async fn test_select_previous(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// { -// // `Select previous` without a selection (selects wordwise) -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); -// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); -// cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); -// } -// { -// // `Select previous` with a selection -// let mut cx = EditorTestContext::new(cx).await; -// cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); - -// cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); - -// cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); - -// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) -// .unwrap(); -// cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); -// } -// } - -// #[gpui::test] -// async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let language = Arc::new(Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// )); - -// let text = r#" -// use mod1::mod2::{mod3, mod4}; - -// fn fn_1(param1: bool, param2: &str) { -// let var1 = "text"; -// } -// "# -// .unindent(); - -// let buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) -// .await; - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), -// DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), -// DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), -// ]); -// }); -// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| { view.selections.display_ranges(cx) }), -// &[ -// DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), -// DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), -// DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), -// ] -// ); - -// view.update(cx, |view, cx| { -// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), -// DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), -// ] -// ); - -// view.update(cx, |view, cx| { -// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] -// ); - -// // Trying to expand the selected syntax node one more time has no effect. -// view.update(cx, |view, cx| { -// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] -// ); - -// view.update(cx, |view, cx| { -// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), -// DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), -// ] -// ); - -// view.update(cx, |view, cx| { -// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), -// DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), -// DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), -// ] -// ); - -// view.update(cx, |view, cx| { -// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), -// DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), -// DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), -// ] -// ); - -// // Trying to shrink the selected syntax node one more time has no effect. -// view.update(cx, |view, cx| { -// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), -// DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), -// DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), -// ] -// ); - -// // Ensure that we keep expanding the selection if the larger selection starts or ends within -// // a fold. -// view.update(cx, |view, cx| { -// view.fold_ranges( -// vec![ -// Point::new(0, 21)..Point::new(0, 24), -// Point::new(3, 20)..Point::new(3, 22), -// ], -// true, -// cx, -// ); -// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); -// }); -// assert_eq!( -// view.update(cx, |view, cx| view.selections.display_ranges(cx)), -// &[ -// DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), -// DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), -// DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), -// ] -// ); -// } - -// #[gpui::test] -// async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let language = Arc::new( -// Language::new( -// LanguageConfig { -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: false, -// newline: true, -// }, -// BracketPair { -// start: "(".to_string(), -// end: ")".to_string(), -// close: false, -// newline: true, -// }, -// ], -// ..Default::default() -// }, -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query( -// r#" -// (_ "(" ")" @end) @indent -// (_ "{" "}" @end) @indent -// "#, -// ) -// .unwrap(), -// ); - -// let text = "fn a() {}"; - -// let buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor -// .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) -// .await; - -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); -// editor.newline(&Newline, cx); -// assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); -// assert_eq!( -// editor.selections.ranges(cx), -// &[ -// Point::new(1, 4)..Point::new(1, 4), -// Point::new(3, 4)..Point::new(3, 4), -// Point::new(5, 0)..Point::new(5, 0) -// ] -// ); -// }); -// } - -// #[gpui::test] -// async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// let language = Arc::new(Language::new( -// LanguageConfig { -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }, -// BracketPair { -// start: "(".to_string(), -// end: ")".to_string(), -// close: true, -// newline: true, -// }, -// BracketPair { -// start: "/*".to_string(), -// end: " */".to_string(), -// close: true, -// newline: true, -// }, -// BracketPair { -// start: "[".to_string(), -// end: "]".to_string(), -// close: false, -// newline: true, -// }, -// BracketPair { -// start: "\"".to_string(), -// end: "\"".to_string(), -// close: true, -// newline: false, -// }, -// ], -// ..Default::default() -// }, -// autoclose_before: "})]".to_string(), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )); - -// let registry = Arc::new(LanguageRegistry::test()); -// registry.add(language.clone()); -// cx.update_buffer(|buffer, cx| { -// buffer.set_language_registry(registry); -// buffer.set_language(Some(language), cx); -// }); - -// cx.set_state( -// &r#" -// 🏀ˇ -// εˇ -// ❤️ˇ -// "# -// .unindent(), -// ); - -// // autoclose multiple nested brackets at multiple cursors -// cx.update_editor(|view, cx| { -// view.handle_input("{", cx); -// view.handle_input("{", cx); -// view.handle_input("{", cx); -// }); -// cx.assert_editor_state( -// &" -// 🏀{{{ˇ}}} -// ε{{{ˇ}}} -// ❤️{{{ˇ}}} -// " -// .unindent(), -// ); - -// // insert a different closing bracket -// cx.update_editor(|view, cx| { -// view.handle_input(")", cx); -// }); -// cx.assert_editor_state( -// &" -// 🏀{{{)ˇ}}} -// ε{{{)ˇ}}} -// ❤️{{{)ˇ}}} -// " -// .unindent(), -// ); - -// // skip over the auto-closed brackets when typing a closing bracket -// cx.update_editor(|view, cx| { -// view.move_right(&MoveRight, cx); -// view.handle_input("}", cx); -// view.handle_input("}", cx); -// view.handle_input("}", cx); -// }); -// cx.assert_editor_state( -// &" -// 🏀{{{)}}}}ˇ -// ε{{{)}}}}ˇ -// ❤️{{{)}}}}ˇ -// " -// .unindent(), -// ); - -// // autoclose multi-character pairs -// cx.set_state( -// &" -// ˇ -// ˇ -// " -// .unindent(), -// ); -// cx.update_editor(|view, cx| { -// view.handle_input("/", cx); -// view.handle_input("*", cx); -// }); -// cx.assert_editor_state( -// &" -// /*ˇ */ -// /*ˇ */ -// " -// .unindent(), -// ); - -// // one cursor autocloses a multi-character pair, one cursor -// // does not autoclose. -// cx.set_state( -// &" -// /ˇ -// ˇ -// " -// .unindent(), -// ); -// cx.update_editor(|view, cx| view.handle_input("*", cx)); -// cx.assert_editor_state( -// &" -// /*ˇ */ -// *ˇ -// " -// .unindent(), -// ); - -// // Don't autoclose if the next character isn't whitespace and isn't -// // listed in the language's "autoclose_before" section. -// cx.set_state("ˇa b"); -// cx.update_editor(|view, cx| view.handle_input("{", cx)); -// cx.assert_editor_state("{ˇa b"); - -// // Don't autoclose if `close` is false for the bracket pair -// cx.set_state("ˇ"); -// cx.update_editor(|view, cx| view.handle_input("[", cx)); -// cx.assert_editor_state("[ˇ"); - -// // Surround with brackets if text is selected -// cx.set_state("«aˇ» b"); -// cx.update_editor(|view, cx| view.handle_input("{", cx)); -// cx.assert_editor_state("{«aˇ»} b"); - -// // Autclose pair where the start and end characters are the same -// cx.set_state("aˇ"); -// cx.update_editor(|view, cx| view.handle_input("\"", cx)); -// cx.assert_editor_state("a\"ˇ\""); -// cx.update_editor(|view, cx| view.handle_input("\"", cx)); -// cx.assert_editor_state("a\"\"ˇ"); -// } - -// #[gpui::test] -// async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// let html_language = Arc::new( -// Language::new( -// LanguageConfig { -// name: "HTML".into(), -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "<".into(), -// end: ">".into(), -// close: true, -// ..Default::default() -// }, -// BracketPair { -// start: "{".into(), -// end: "}".into(), -// close: true, -// ..Default::default() -// }, -// BracketPair { -// start: "(".into(), -// end: ")".into(), -// close: true, -// ..Default::default() -// }, -// ], -// ..Default::default() -// }, -// autoclose_before: "})]>".into(), -// ..Default::default() -// }, -// Some(tree_sitter_html::language()), -// ) -// .with_injection_query( -// r#" -// (script_element -// (raw_text) @content -// (#set! "language" "javascript")) -// "#, -// ) -// .unwrap(), -// ); - -// let javascript_language = Arc::new(Language::new( -// LanguageConfig { -// name: "JavaScript".into(), -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "/*".into(), -// end: " */".into(), -// close: true, -// ..Default::default() -// }, -// BracketPair { -// start: "{".into(), -// end: "}".into(), -// close: true, -// ..Default::default() -// }, -// BracketPair { -// start: "(".into(), -// end: ")".into(), -// close: true, -// ..Default::default() -// }, -// ], -// ..Default::default() -// }, -// autoclose_before: "})]>".into(), -// ..Default::default() -// }, -// Some(tree_sitter_typescript::language_tsx()), -// )); - -// let registry = Arc::new(LanguageRegistry::test()); -// registry.add(html_language.clone()); -// registry.add(javascript_language.clone()); - -// cx.update_buffer(|buffer, cx| { -// buffer.set_language_registry(registry); -// buffer.set_language(Some(html_language), cx); -// }); - -// cx.set_state( -// &r#" -// ˇ -// -// ˇ -// "# -// .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()] -// ); -// }); - -// // 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(), -// ); -// } - -// #[gpui::test] -// async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// let rust_language = Arc::new( -// Language::new( -// LanguageConfig { -// name: "Rust".into(), -// brackets: serde_json::from_value(json!([ -// { "start": "{", "end": "}", "close": true, "newline": true }, -// { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] }, -// ])) -// .unwrap(), -// autoclose_before: "})]>".into(), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_override_query("(string_literal) @string") -// .unwrap(), -// ); - -// let registry = Arc::new(LanguageRegistry::test()); -// registry.add(rust_language.clone()); - -// cx.update_buffer(|buffer, cx| { -// buffer.set_language_registry(registry); -// buffer.set_language(Some(rust_language), cx); -// }); - -// cx.set_state( -// &r#" -// let x = ˇ -// "# -// .unindent(), -// ); - -// // Inserting a quotation mark. A closing quotation mark is automatically inserted. -// cx.update_editor(|editor, cx| { -// editor.handle_input("\"", cx); -// }); -// cx.assert_editor_state( -// &r#" -// let x = "ˇ" -// "# -// .unindent(), -// ); - -// // Inserting another quotation mark. The cursor moves across the existing -// // automatically-inserted quotation mark. -// cx.update_editor(|editor, cx| { -// editor.handle_input("\"", cx); -// }); -// cx.assert_editor_state( -// &r#" -// let x = ""ˇ -// "# -// .unindent(), -// ); - -// // Reset -// cx.set_state( -// &r#" -// let x = ˇ -// "# -// .unindent(), -// ); - -// // Inserting a quotation mark inside of a string. A second quotation mark is not inserted. -// cx.update_editor(|editor, cx| { -// editor.handle_input("\"", cx); -// editor.handle_input(" ", cx); -// editor.move_left(&Default::default(), cx); -// editor.handle_input("\\", cx); -// editor.handle_input("\"", cx); -// }); -// cx.assert_editor_state( -// &r#" -// let x = "\"ˇ " -// "# -// .unindent(), -// ); - -// // Inserting a closing quotation mark at the position of an automatically-inserted quotation -// // mark. Nothing is inserted. -// cx.update_editor(|editor, cx| { -// editor.move_right(&Default::default(), cx); -// editor.handle_input("\"", cx); -// }); -// cx.assert_editor_state( -// &r#" -// let x = "\" "ˇ -// "# -// .unindent(), -// ); -// } - -// #[gpui::test] -// async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let language = Arc::new(Language::new( -// LanguageConfig { -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }, -// BracketPair { -// start: "/* ".to_string(), -// end: "*/".to_string(), -// close: true, -// ..Default::default() -// }, -// ], -// ..Default::default() -// }, -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )); - -// let text = r#" -// a -// b -// c -// "# -// .unindent(); - -// let buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) -// .await; - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), -// ]) -// }); - -// view.handle_input("{", cx); -// view.handle_input("{", cx); -// view.handle_input("{", cx); -// assert_eq!( -// view.text(cx), -// " -// {{{a}}} -// {{{b}}} -// {{{c}}} -// " -// .unindent() -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4), -// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4), -// DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4) -// ] -// ); - -// view.undo(&Undo, cx); -// view.undo(&Undo, cx); -// view.undo(&Undo, cx); -// assert_eq!( -// view.text(cx), -// " -// a -// b -// c -// " -// .unindent() -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) -// ] -// ); - -// // Ensure inserting the first character of a multi-byte bracket pair -// // doesn't surround the selections with the bracket. -// view.handle_input("/", cx); -// assert_eq!( -// view.text(cx), -// " -// / -// / -// / -// " -// .unindent() -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) -// ] -// ); - -// view.undo(&Undo, cx); -// assert_eq!( -// view.text(cx), -// " -// a -// b -// c -// " -// .unindent() -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) -// ] -// ); - -// // Ensure inserting the last character of a multi-byte bracket pair -// // doesn't surround the selections with the bracket. -// view.handle_input("*", cx); -// assert_eq!( -// view.text(cx), -// " -// * -// * -// * -// " -// .unindent() -// ); -// assert_eq!( -// view.selections.display_ranges(cx), -// [ -// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), -// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), -// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) -// ] -// ); -// }); -// } - -// #[gpui::test] -// async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let language = Arc::new(Language::new( -// LanguageConfig { -// brackets: BracketPairConfig { -// pairs: vec![BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }], -// ..Default::default() -// }, -// autoclose_before: "}".to_string(), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )); - -// let text = r#" -// a -// b -// c -// "# -// .unindent(); - -// let buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor -// .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) -// .await; - -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([ -// Point::new(0, 1)..Point::new(0, 1), -// Point::new(1, 1)..Point::new(1, 1), -// Point::new(2, 1)..Point::new(2, 1), -// ]) -// }); - -// editor.handle_input("{", cx); -// editor.handle_input("{", cx); -// editor.handle_input("_", cx); -// assert_eq!( -// editor.text(cx), -// " -// a{{_}} -// b{{_}} -// c{{_}} -// " -// .unindent() -// ); -// assert_eq!( -// editor.selections.ranges::(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) -// ] -// ); -// }); -// } - -// #[gpui::test] -// async fn test_snippets(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let (text, insertion_ranges) = marked_text_ranges( -// indoc! {" -// a.ˇ b -// a.ˇ b -// a.ˇ b -// "}, -// false, -// ); - -// let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - -// editor.update(cx, |editor, cx| { -// let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); - -// editor -// .insert_snippet(&insertion_ranges, snippet, cx) -// .unwrap(); - -// fn assert(editor: &mut Editor, cx: &mut ViewContext, 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); -// } - -// assert( -// editor, -// cx, -// indoc! {" -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// "}, -// ); - -// // Can't move earlier than the first tab stop -// assert!(!editor.move_to_prev_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// "}, -// ); - -// assert!(editor.move_to_next_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// "}, -// ); - -// editor.move_to_prev_snippet_tabstop(cx); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// "}, -// ); - -// assert!(editor.move_to_next_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// "}, -// ); -// assert!(editor.move_to_next_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// "}, -// ); - -// // As soon as the last tab stop is reached, snippet state is gone -// editor.move_to_prev_snippet_tabstop(cx); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// "}, -// ); -// }); -// } - -// #[gpui::test] -// async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_file("/file.rs", Default::default()).await; - -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) -// .await -// .unwrap(); - -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); - -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); -// assert!(cx.read(|cx| editor.is_dirty(cx))); - -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// fake_server -// .handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// assert_eq!(params.options.tab_size, 4); -// Ok(Some(vec![lsp::TextEdit::new( -// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), -// ", ".to_string(), -// )])) -// }) -// .next() -// .await; -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one, two\nthree\n" -// ); -// assert!(!cx.read(|cx| editor.is_dirty(cx))); - -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); -// assert!(cx.read(|cx| editor.is_dirty(cx))); - -// // Ensure we can still save even if formatting hangs. -// fake_server.handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// futures::future::pending::<()>().await; -// unreachable!() -// }); -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// cx.foreground().advance_clock(super::FORMAT_TIMEOUT); -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one\ntwo\nthree\n" -// ); -// assert!(!cx.read(|cx| editor.is_dirty(cx))); - -// // Set rust language override and assert overridden tabsize is sent to language server -// update_test_language_settings(cx, |settings| { -// settings.languages.insert( -// "Rust".into(), -// LanguageSettingsContent { -// tab_size: NonZeroU32::new(8), -// ..Default::default() -// }, -// ); -// }); - -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// fake_server -// .handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// assert_eq!(params.options.tab_size, 8); -// Ok(Some(vec![])) -// }) -// .next() -// .await; -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// } - -// #[gpui::test] -// async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_range_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_file("/file.rs", Default::default()).await; - -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) -// .await -// .unwrap(); - -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); - -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); -// assert!(cx.read(|cx| editor.is_dirty(cx))); - -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// fake_server -// .handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// assert_eq!(params.options.tab_size, 4); -// Ok(Some(vec![lsp::TextEdit::new( -// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), -// ", ".to_string(), -// )])) -// }) -// .next() -// .await; -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one, two\nthree\n" -// ); -// assert!(!cx.read(|cx| editor.is_dirty(cx))); - -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); -// assert!(cx.read(|cx| editor.is_dirty(cx))); - -// // Ensure we can still save even if formatting hangs. -// fake_server.handle_request::( -// move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// futures::future::pending::<()>().await; -// unreachable!() -// }, -// ); -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// cx.foreground().advance_clock(super::FORMAT_TIMEOUT); -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one\ntwo\nthree\n" -// ); -// assert!(!cx.read(|cx| editor.is_dirty(cx))); - -// // Set rust language override and assert overridden tabsize is sent to language server -// update_test_language_settings(cx, |settings| { -// settings.languages.insert( -// "Rust".into(), -// LanguageSettingsContent { -// tab_size: NonZeroU32::new(8), -// ..Default::default() -// }, -// ); -// }); - -// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); -// fake_server -// .handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// assert_eq!(params.options.tab_size, 8); -// Ok(Some(vec![])) -// }) -// .next() -// .await; -// cx.foreground().start_waiting(); -// save.await.unwrap(); -// } - -// #[gpui::test] -// async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// // Enable Prettier formatting for the same buffer, and ensure -// // LSP is called instead of Prettier. -// prettier_parser_name: Some("test_parser".to_string()), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_file("/file.rs", Default::default()).await; - -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// project.update(cx, |project, _| { -// project.languages().add(Arc::new(language)); -// }); -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) -// .await -// .unwrap(); - -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); - -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - -// let format = editor.update(cx, |editor, cx| { -// editor.perform_format(project.clone(), FormatTrigger::Manual, cx) -// }); -// fake_server -// .handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// assert_eq!(params.options.tab_size, 4); -// Ok(Some(vec![lsp::TextEdit::new( -// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), -// ", ".to_string(), -// )])) -// }) -// .next() -// .await; -// cx.foreground().start_waiting(); -// format.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one, two\nthree\n" -// ); - -// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); -// // Ensure we don't lock if formatting hangs. -// fake_server.handle_request::(move |params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/file.rs").unwrap() -// ); -// futures::future::pending::<()>().await; -// unreachable!() -// }); -// let format = editor.update(cx, |editor, cx| { -// editor.perform_format(project, FormatTrigger::Manual, cx) -// }); -// cx.foreground().advance_clock(super::FORMAT_TIMEOUT); -// cx.foreground().start_waiting(); -// format.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// "one\ntwo\nthree\n" -// ); -// } - -// #[gpui::test] -// async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// document_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.set_state(indoc! {" -// one.twoˇ -// "}); - -// // The format request takes a long time. When it completes, it inserts -// // a newline and an indent before the `.` -// cx.lsp -// .handle_request::(move |_, cx| { -// let executor = cx.background(); -// async move { -// executor.timer(Duration::from_millis(100)).await; -// Ok(Some(vec![lsp::TextEdit { -// range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)), -// new_text: "\n ".into(), -// }])) -// } -// }); - -// // Submit a format request. -// let format_1 = cx -// .update_editor(|editor, cx| editor.format(&Format, cx)) -// .unwrap(); -// cx.foreground().run_until_parked(); - -// // Submit a second format request. -// let format_2 = cx -// .update_editor(|editor, cx| editor.format(&Format, cx)) -// .unwrap(); -// cx.foreground().run_until_parked(); - -// // Wait for both format requests to complete -// cx.foreground().advance_clock(Duration::from_millis(200)); -// cx.foreground().start_waiting(); -// format_1.await.unwrap(); -// cx.foreground().start_waiting(); -// format_2.await.unwrap(); - -// // The formatting edits only happens once. -// cx.assert_editor_state(indoc! {" -// one -// .twoˇ -// "}); -// } - -// #[gpui::test] -// async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.formatter = Some(language_settings::Formatter::Auto) -// }); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// document_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Set up a buffer white some trailing whitespace and no trailing newline. -// cx.set_state( -// &[ -// "one ", // -// "twoˇ", // -// "three ", // -// "four", // -// ] -// .join("\n"), -// ); - -// // Submit a format request. -// let format = cx -// .update_editor(|editor, cx| editor.format(&Format, cx)) -// .unwrap(); - -// // Record which buffer changes have been sent to the language server -// let buffer_changes = Arc::new(Mutex::new(Vec::new())); -// cx.lsp -// .handle_notification::({ -// let buffer_changes = buffer_changes.clone(); -// move |params, _| { -// buffer_changes.lock().extend( -// params -// .content_changes -// .into_iter() -// .map(|e| (e.range.unwrap(), e.text)), -// ); -// } -// }); - -// // Handle formatting requests to the language server. -// cx.lsp.handle_request::({ -// let buffer_changes = buffer_changes.clone(); -// move |_, _| { -// // When formatting is requested, trailing whitespace has already been stripped, -// // and the trailing newline has already been added. -// assert_eq!( -// &buffer_changes.lock()[1..], -// &[ -// ( -// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)), -// "".into() -// ), -// ( -// lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), -// "".into() -// ), -// ( -// lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)), -// "\n".into() -// ), -// ] -// ); - -// // Insert blank lines between each line of the buffer. -// async move { -// Ok(Some(vec![ -// lsp::TextEdit { -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), -// new_text: "\n".into(), -// }, -// lsp::TextEdit { -// range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)), -// new_text: "\n".into(), -// }, -// ])) -// } -// } -// }); - -// // After formatting the buffer, the trailing whitespace is stripped, -// // a newline is appended, and the edits provided by the language server -// // have been applied. -// format.await.unwrap(); -// cx.assert_editor_state( -// &[ -// "one", // -// "", // -// "twoˇ", // -// "", // -// "three", // -// "four", // -// "", // -// ] -// .join("\n"), -// ); - -// // Undoing the formatting undoes the trailing whitespace removal, the -// // trailing newline, and the LSP edits. -// cx.update_buffer(|buffer, cx| buffer.undo(cx)); -// cx.assert_editor_state( -// &[ -// "one ", // -// "twoˇ", // -// "three ", // -// "four", // -// ] -// .join("\n"), -// ); -// } - -// #[gpui::test] -// async fn test_completion(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string(), ":".to_string()]), -// resolve_provider: Some(true), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.set_state(indoc! {" -// oneˇ -// two -// three -// "}); -// cx.simulate_keystroke("."); -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.|<> -// two -// three -// "}, -// vec!["first_completion", "second_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor.context_menu_next(&Default::default(), cx); -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {" -// one.second_completionˇ -// two -// three -// "}); - -// handle_resolve_completion_request( -// &mut cx, -// Some(vec![ -// ( -// //This overlaps with the primary completion edit which is -// //misbehavior from the LSP spec, test that we filter it out -// indoc! {" -// one.second_ˇcompletion -// two -// threeˇ -// "}, -// "overlapping additional edit", -// ), -// ( -// indoc! {" -// one.second_completion -// two -// threeˇ -// "}, -// "\nadditional edit", -// ), -// ]), -// ) -// .await; -// apply_additional_edits.await.unwrap(); -// cx.assert_editor_state(indoc! {" -// one.second_completionˇ -// two -// three -// additional edit -// "}); - -// cx.set_state(indoc! {" -// one.second_completion -// twoˇ -// threeˇ -// additional edit -// "}); -// cx.simulate_keystroke(" "); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.simulate_keystroke("s"); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); - -// cx.assert_editor_state(indoc! {" -// one.second_completion -// two sˇ -// three sˇ -// additional edit -// "}); -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.second_completion -// two s -// three -// additional edit -// "}, -// vec!["fourth_completion", "fifth_completion", "sixth_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; - -// cx.simulate_keystroke("i"); - -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.second_completion -// two si -// three -// additional edit -// "}, -// vec!["fourth_completion", "fifth_completion", "sixth_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; - -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {" -// one.second_completion -// two sixth_completionˇ -// three sixth_completionˇ -// additional edit -// "}); - -// handle_resolve_completion_request(&mut cx, None).await; -// apply_additional_edits.await.unwrap(); - -// cx.update(|cx| { -// cx.update_global::(|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_toggle_comment(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; -// let language = Arc::new(Language::new( -// LanguageConfig { -// line_comment: Some("// ".into()), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )); -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - -// // If multiple selections intersect a line, the line is only toggled once. -// cx.set_state(indoc! {" -// fn a() { -// «//b(); -// ˇ»// «c(); -// //ˇ» d(); -// } -// "}); - -// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - -// cx.assert_editor_state(indoc! {" -// fn a() { -// «b(); -// c(); -// ˇ» d(); -// } -// "}); - -// // The comment prefix is inserted at the same column for every line in a -// // selection. -// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - -// cx.assert_editor_state(indoc! {" -// fn a() { -// // «b(); -// // c(); -// ˇ»// d(); -// } -// "}); - -// // If a selection ends at the beginning of a line, that line is not toggled. -// cx.set_selections_state(indoc! {" -// fn a() { -// // b(); -// «// c(); -// ˇ» // d(); -// } -// "}); - -// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - -// cx.assert_editor_state(indoc! {" -// fn a() { -// // b(); -// «c(); -// ˇ» // d(); -// } -// "}); - -// // If a selection span a single line and is empty, the line is toggled. -// cx.set_state(indoc! {" -// fn a() { -// a(); -// b(); -// ˇ -// } -// "}); - -// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - -// cx.assert_editor_state(indoc! {" -// fn a() { -// a(); -// b(); -// //•ˇ -// } -// "}); - -// // If a selection span multiple lines, empty lines are not toggled. -// cx.set_state(indoc! {" -// fn a() { -// «a(); - -// c();ˇ» -// } -// "}); - -// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - -// cx.assert_editor_state(indoc! {" -// fn a() { -// // «a(); - -// // c();ˇ» -// } -// "}); -// } - -// #[gpui::test] -// async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let language = Arc::new(Language::new( -// LanguageConfig { -// line_comment: Some("// ".into()), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )); - -// let registry = Arc::new(LanguageRegistry::test()); -// registry.add(language.clone()); - -// let mut cx = EditorTestContext::new(cx).await; -// cx.update_buffer(|buffer, cx| { -// buffer.set_language_registry(registry); -// buffer.set_language(Some(language), cx); -// }); - -// let toggle_comments = &ToggleComments { -// advance_downwards: true, -// }; - -// // Single cursor on one line -> advance -// // Cursor moves horizontally 3 characters as well on non-blank line -// cx.set_state(indoc!( -// "fn a() { -// ˇdog(); -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // dog(); -// catˇ(); -// }" -// )); - -// // Single selection on one line -> don't advance -// cx.set_state(indoc!( -// "fn a() { -// «dog()ˇ»; -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // «dog()ˇ»; -// cat(); -// }" -// )); - -// // Multiple cursors on one line -> advance -// cx.set_state(indoc!( -// "fn a() { -// ˇdˇog(); -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // dog(); -// catˇ(ˇ); -// }" -// )); - -// // Multiple cursors on one line, with selection -> don't advance -// cx.set_state(indoc!( -// "fn a() { -// ˇdˇog«()ˇ»; -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // ˇdˇog«()ˇ»; -// cat(); -// }" -// )); - -// // Single cursor on one line -> advance -// // Cursor moves to column 0 on blank line -// cx.set_state(indoc!( -// "fn a() { -// ˇdog(); - -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // dog(); -// ˇ -// cat(); -// }" -// )); - -// // Single cursor on one line -> advance -// // Cursor starts and ends at column 0 -// cx.set_state(indoc!( -// "fn a() { -// ˇ dog(); -// cat(); -// }" -// )); -// cx.update_editor(|editor, cx| { -// editor.toggle_comments(toggle_comments, cx); -// }); -// cx.assert_editor_state(indoc!( -// "fn a() { -// // dog(); -// ˇ cat(); -// }" -// )); -// } - -// #[gpui::test] -// async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// let html_language = Arc::new( -// Language::new( -// LanguageConfig { -// name: "HTML".into(), -// block_comment: Some(("".into())), -// ..Default::default() -// }, -// Some(tree_sitter_html::language()), -// ) -// .with_injection_query( -// r#" -// (script_element -// (raw_text) @content -// (#set! "language" "javascript")) -// "#, -// ) -// .unwrap(), -// ); - -// let javascript_language = Arc::new(Language::new( -// LanguageConfig { -// name: "JavaScript".into(), -// line_comment: Some("// ".into()), -// ..Default::default() -// }, -// Some(tree_sitter_typescript::language_tsx()), -// )); - -// let registry = Arc::new(LanguageRegistry::test()); -// registry.add(html_language.clone()); -// registry.add(javascript_language.clone()); - -// cx.update_buffer(|buffer, cx| { -// buffer.set_language_registry(registry); -// buffer.set_language(Some(html_language), cx); -// }); - -// // Toggle comments for empty selections -// cx.set_state( -// &r#" -//

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, |_| {}); - -// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// buffer.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(0, 4), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(1, 0)..Point::new(1, 4), -// primary: None, -// }, -// ], -// cx, -// ); -// assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb"); -// multibuffer -// }); - -// let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); -// view.update(cx, |view, cx| { -// assert_eq!(view.text(cx), "aaaa\nbbbb"); -// view.change_selections(None, cx, |s| { -// s.select_ranges([ -// Point::new(0, 0)..Point::new(0, 0), -// Point::new(1, 0)..Point::new(1, 0), -// ]) -// }); - -// view.handle_input("X", cx); -// assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); -// assert_eq!( -// view.selections.ranges(cx), -// [ -// Point::new(0, 1)..Point::new(0, 1), -// Point::new(1, 1)..Point::new(1, 1), -// ] -// ); - -// // Ensure the cursor's head is respected when deleting across an excerpt boundary. -// view.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) -// }); -// view.backspace(&Default::default(), cx); -// assert_eq!(view.text(cx), "Xa\nbbb"); -// assert_eq!( -// view.selections.ranges(cx), -// [Point::new(1, 0)..Point::new(1, 0)] -// ); - -// view.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) -// }); -// view.backspace(&Default::default(), cx); -// assert_eq!(view.text(cx), "X\nbb"); -// assert_eq!( -// view.selections.ranges(cx), -// [Point::new(0, 1)..Point::new(0, 1)] -// ); -// }); -// } - -// #[gpui::test] -// fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let markers = vec![('[', ']').into(), ('(', ')').into()]; -// let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( -// indoc! {" -// [aaaa -// (bbbb] -// cccc)", -// }, -// markers.clone(), -// ); -// let excerpt_ranges = markers.into_iter().map(|marker| { -// let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); -// ExcerptRange { -// context, -// primary: None, -// } -// }); -// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text)); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts(buffer, excerpt_ranges, cx); -// multibuffer -// }); - -// let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); -// view.update(cx, |view, cx| { -// let (expected_text, selection_ranges) = marked_text_ranges( -// indoc! {" -// aaaa -// bˇbbb -// bˇbbˇb -// cccc" -// }, -// true, -// ); -// assert_eq!(view.text(cx), expected_text); -// view.change_selections(None, cx, |s| s.select_ranges(selection_ranges)); - -// view.handle_input("X", cx); - -// let (expected_text, expected_selections) = marked_text_ranges( -// indoc! {" -// aaaa -// bXˇbbXb -// bXˇbbXˇb -// cccc" -// }, -// false, -// ); -// assert_eq!(view.text(cx), expected_text); -// assert_eq!(view.selections.ranges(cx), expected_selections); - -// view.newline(&Newline, cx); -// let (expected_text, expected_selections) = marked_text_ranges( -// indoc! {" -// aaaa -// bX -// ˇbbX -// b -// bX -// ˇbbX -// ˇb -// cccc" -// }, -// false, -// ); -// assert_eq!(view.text(cx), expected_text); -// assert_eq!(view.selections.ranges(cx), expected_selections); -// }); -// } - -// #[gpui::test] -// fn test_refresh_selections(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); -// let mut excerpt1_id = None; -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// excerpt1_id = multibuffer -// .push_excerpts( -// buffer.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 4), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(1, 0)..Point::new(2, 4), -// primary: None, -// }, -// ], -// cx, -// ) -// .into_iter() -// .next(); -// assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); -// multibuffer -// }); - -// let editor = cx -// .add_window(|cx| { -// let mut editor = build_editor(multibuffer.clone(), cx); -// let snapshot = editor.snapshot(cx); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) -// }); -// editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); -// assert_eq!( -// editor.selections.ranges(cx), -// [ -// Point::new(1, 3)..Point::new(1, 3), -// Point::new(2, 1)..Point::new(2, 1), -// ] -// ); -// editor -// }) -// .root(cx); - -// // Refreshing selections is a no-op when excerpts haven't changed. -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.refresh()); -// assert_eq!( -// editor.selections.ranges(cx), -// [ -// Point::new(1, 3)..Point::new(1, 3), -// Point::new(2, 1)..Point::new(2, 1), -// ] -// ); -// }); - -// multibuffer.update(cx, |multibuffer, cx| { -// multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); -// }); -// editor.update(cx, |editor, cx| { -// // Removing an excerpt causes the first selection to become degenerate. -// assert_eq!( -// editor.selections.ranges(cx), -// [ -// Point::new(0, 0)..Point::new(0, 0), -// Point::new(0, 1)..Point::new(0, 1) -// ] -// ); - -// // Refreshing selections will relocate the first selection to the original buffer -// // location. -// editor.change_selections(None, cx, |s| s.refresh()); -// assert_eq!( -// editor.selections.ranges(cx), -// [ -// Point::new(0, 1)..Point::new(0, 1), -// Point::new(0, 3)..Point::new(0, 3) -// ] -// ); -// assert!(editor.selections.pending_anchor().is_some()); -// }); -// } - -// #[gpui::test] -// fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); -// let mut excerpt1_id = None; -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// excerpt1_id = multibuffer -// .push_excerpts( -// buffer.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 4), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(1, 0)..Point::new(2, 4), -// primary: None, -// }, -// ], -// cx, -// ) -// .into_iter() -// .next(); -// assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); -// multibuffer -// }); - -// let editor = cx -// .add_window(|cx| { -// let mut editor = build_editor(multibuffer.clone(), cx); -// let snapshot = editor.snapshot(cx); -// editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); -// assert_eq!( -// editor.selections.ranges(cx), -// [Point::new(1, 3)..Point::new(1, 3)] -// ); -// editor -// }) -// .root(cx); - -// multibuffer.update(cx, |multibuffer, cx| { -// multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); -// }); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// editor.selections.ranges(cx), -// [Point::new(0, 0)..Point::new(0, 0)] -// ); - -// // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. -// editor.change_selections(None, cx, |s| s.refresh()); -// assert_eq!( -// editor.selections.ranges(cx), -// [Point::new(0, 3)..Point::new(0, 3)] -// ); -// assert!(editor.selections.pending_anchor().is_some()); -// }); -// } - -// #[gpui::test] -// async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let language = Arc::new( -// Language::new( -// LanguageConfig { -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }, -// BracketPair { -// start: "/* ".to_string(), -// end: " */".to_string(), -// close: true, -// newline: true, -// }, -// ], -// ..Default::default() -// }, -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_indents_query("") -// .unwrap(), -// ); - -// let text = concat!( -// "{ }\n", // -// " x\n", // -// " /* */\n", // -// "x\n", // -// "{{} }\n", // -// ); - -// let buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) -// .await; - -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), -// DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), -// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), -// ]) -// }); -// view.newline(&Newline, cx); - -// assert_eq!( -// view.buffer().read(cx).read(cx).text(), -// concat!( -// "{ \n", // Suppress rustfmt -// "\n", // -// "}\n", // -// " x\n", // -// " /* \n", // -// " \n", // -// " */\n", // -// "x\n", // -// "{{} \n", // -// "}\n", // -// ) -// ); -// }); -// } - -// #[gpui::test] -// fn test_highlighted_ranges(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); -// build_editor(buffer.clone(), cx) -// }) -// .root(cx); - -// editor.update(cx, |editor, cx| { -// struct Type1; -// struct Type2; - -// let buffer = editor.buffer.read(cx).snapshot(cx); - -// let anchor_range = -// |range: Range| buffer.anchor_after(range.start)..buffer.anchor_after(range.end); - -// editor.highlight_background::( -// vec![ -// anchor_range(Point::new(2, 1)..Point::new(2, 3)), -// anchor_range(Point::new(4, 2)..Point::new(4, 4)), -// anchor_range(Point::new(6, 3)..Point::new(6, 5)), -// anchor_range(Point::new(8, 4)..Point::new(8, 6)), -// ], -// |_| Hsla::red(), -// cx, -// ); -// editor.highlight_background::( -// vec![ -// anchor_range(Point::new(3, 2)..Point::new(3, 5)), -// anchor_range(Point::new(5, 3)..Point::new(5, 6)), -// anchor_range(Point::new(7, 4)..Point::new(7, 7)), -// anchor_range(Point::new(9, 5)..Point::new(9, 8)), -// ], -// |_| Hsla::green(), -// cx, -// ); - -// let snapshot = editor.snapshot(cx); -// let mut highlighted_ranges = editor.background_highlights_in_range( -// anchor_range(Point::new(3, 4)..Point::new(7, 4)), -// &snapshot, -// theme::current(cx).as_ref(), -// ); -// // Enforce a consistent ordering based on color without relying on the ordering of the -// // highlight's `TypeId` which is non-deterministic. -// highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); -// assert_eq!( -// highlighted_ranges, -// &[ -// ( -// DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), -// Hsla::green(), -// ), -// ( -// DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), -// Hsla::green(), -// ), -// ( -// DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), -// Hsla::red(), -// ), -// ( -// DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), -// Hsla::red(), -// ), -// ] -// ); -// assert_eq!( -// editor.background_highlights_in_range( -// anchor_range(Point::new(5, 6)..Point::new(6, 4)), -// &snapshot, -// theme::current(cx).as_ref(), -// ), -// &[( -// DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), -// Hsla::red(), -// )] -// ); -// }); -// } - -// #[gpui::test] -// async fn test_following(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let fs = FakeFs::new(cx.background()); -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - -// let buffer = project.update(cx, |project, cx| { -// let buffer = project -// .create_buffer(&sample_text(16, 8, 'a'), None, cx) -// .unwrap(); -// cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) -// }); -// let leader = cx -// .add_window(|cx| build_editor(buffer.clone(), cx)) -// .root(cx); -// let follower = cx -// .update(|cx| { -// cx.add_window( -// WindowOptions { -// bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), -// ..Default::default() -// }, -// |cx| build_editor(buffer.clone(), cx), -// ) -// }) -// .root(cx); - -// let is_still_following = Rc::new(RefCell::new(true)); -// let follower_edit_event_count = Rc::new(RefCell::new(0)); -// let pending_update = Rc::new(RefCell::new(None)); -// follower.update(cx, { -// let update = pending_update.clone(); -// let is_still_following = is_still_following.clone(); -// let follower_edit_event_count = follower_edit_event_count.clone(); -// |_, cx| { -// cx.subscribe(&leader, move |_, leader, event, cx| { -// leader -// .read(cx) -// .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); -// }) -// .detach(); - -// cx.subscribe(&follower, move |_, _, event, cx| { -// if Editor::should_unfollow_on_event(event, cx) { -// *is_still_following.borrow_mut() = false; -// } -// if let Event::BufferEdited = event { -// *follower_edit_event_count.borrow_mut() += 1; -// } -// }) -// .detach(); -// } -// }); - -// // Update the selections only -// leader.update(cx, |leader, cx| { -// leader.change_selections(None, cx, |s| s.select_ranges([1..1])); -// }); -// follower -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) -// }) -// .await -// .unwrap(); -// follower.read_with(cx, |follower, cx| { -// assert_eq!(follower.selections.ranges(cx), vec![1..1]); -// }); -// assert_eq!(*is_still_following.borrow(), true); -// assert_eq!(*follower_edit_event_count.borrow(), 0); - -// // Update the scroll position only -// leader.update(cx, |leader, cx| { -// leader.set_scroll_position(vec2f(1.5, 3.5), cx); -// }); -// follower -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!( -// follower.update(cx, |follower, cx| follower.scroll_position(cx)), -// vec2f(1.5, 3.5) -// ); -// assert_eq!(*is_still_following.borrow(), true); -// assert_eq!(*follower_edit_event_count.borrow(), 0); - -// // Update the selections and scroll position. The follower's scroll position is updated -// // via autoscroll, not via the leader's exact scroll position. -// leader.update(cx, |leader, cx| { -// leader.change_selections(None, cx, |s| s.select_ranges([0..0])); -// leader.request_autoscroll(Autoscroll::newest(), cx); -// leader.set_scroll_position(vec2f(1.5, 3.5), cx); -// }); -// follower -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) -// }) -// .await -// .unwrap(); -// follower.update(cx, |follower, cx| { -// assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0)); -// assert_eq!(follower.selections.ranges(cx), vec![0..0]); -// }); -// assert_eq!(*is_still_following.borrow(), true); - -// // Creating a pending selection that precedes another selection -// leader.update(cx, |leader, cx| { -// leader.change_selections(None, cx, |s| s.select_ranges([1..1])); -// leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx); -// }); -// follower -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) -// }) -// .await -// .unwrap(); -// follower.read_with(cx, |follower, cx| { -// assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); -// }); -// assert_eq!(*is_still_following.borrow(), true); - -// // Extend the pending selection so that it surrounds another selection -// leader.update(cx, |leader, cx| { -// leader.extend_selection(DisplayPoint::new(0, 2), 1, cx); -// }); -// follower -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) -// }) -// .await -// .unwrap(); -// follower.read_with(cx, |follower, cx| { -// assert_eq!(follower.selections.ranges(cx), vec![0..2]); -// }); - -// // Scrolling locally breaks the follow -// follower.update(cx, |follower, cx| { -// let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0); -// follower.set_scroll_anchor( -// ScrollAnchor { -// anchor: top_anchor, -// offset: vec2f(0.0, 0.5), -// }, -// cx, -// ); -// }); -// assert_eq!(*is_still_following.borrow(), false); -// } - -// #[gpui::test] -// async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let fs = FakeFs::new(cx.background()); -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - -// let leader = pane.update(cx, |_, cx| { -// let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); -// cx.add_view(|cx| build_editor(multibuffer.clone(), cx)) -// }); - -// // Start following the editor when it has no excerpts. -// let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); -// let follower_1 = cx -// .update(|cx| { -// Editor::from_state_proto( -// pane.clone(), -// workspace.clone(), -// ViewId { -// creator: Default::default(), -// id: 0, -// }, -// &mut state_message, -// cx, -// ) -// }) -// .unwrap() -// .await -// .unwrap(); - -// let update_message = Rc::new(RefCell::new(None)); -// follower_1.update(cx, { -// let update = update_message.clone(); -// |_, cx| { -// cx.subscribe(&leader, move |_, leader, event, cx| { -// leader -// .read(cx) -// .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); -// }) -// .detach(); -// } -// }); - -// let (buffer_1, buffer_2) = project.update(cx, |project, cx| { -// ( -// project -// .create_buffer("abc\ndef\nghi\njkl\n", None, cx) -// .unwrap(), -// project -// .create_buffer("mno\npqr\nstu\nvwx\n", None, cx) -// .unwrap(), -// ) -// }); - -// // Insert some excerpts. -// leader.update(cx, |leader, cx| { -// leader.buffer.update(cx, |multibuffer, cx| { -// let excerpt_ids = multibuffer.push_excerpts( -// buffer_1.clone(), -// [ -// ExcerptRange { -// context: 1..6, -// primary: None, -// }, -// ExcerptRange { -// context: 12..15, -// primary: None, -// }, -// ExcerptRange { -// context: 0..3, -// primary: None, -// }, -// ], -// cx, -// ); -// multibuffer.insert_excerpts_after( -// excerpt_ids[0], -// buffer_2.clone(), -// [ -// ExcerptRange { -// context: 8..12, -// primary: None, -// }, -// ExcerptRange { -// context: 0..6, -// primary: None, -// }, -// ], -// cx, -// ); -// }); -// }); - -// // Apply the update of adding the excerpts. -// follower_1 -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!( -// follower_1.read_with(cx, |editor, cx| editor.text(cx)), -// leader.read_with(cx, |editor, cx| editor.text(cx)) -// ); -// update_message.borrow_mut().take(); - -// // Start following separately after it already has excerpts. -// let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); -// let follower_2 = cx -// .update(|cx| { -// Editor::from_state_proto( -// pane.clone(), -// workspace.clone(), -// ViewId { -// creator: Default::default(), -// id: 0, -// }, -// &mut state_message, -// cx, -// ) -// }) -// .unwrap() -// .await -// .unwrap(); -// assert_eq!( -// follower_2.read_with(cx, |editor, cx| editor.text(cx)), -// leader.read_with(cx, |editor, cx| editor.text(cx)) -// ); - -// // Remove some excerpts. -// leader.update(cx, |leader, cx| { -// leader.buffer.update(cx, |multibuffer, cx| { -// let excerpt_ids = multibuffer.excerpt_ids(); -// multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx); -// multibuffer.remove_excerpts([excerpt_ids[0]], cx); -// }); -// }); - -// // Apply the update of removing the excerpts. -// follower_1 -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) -// }) -// .await -// .unwrap(); -// follower_2 -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) -// }) -// .await -// .unwrap(); -// update_message.borrow_mut().take(); -// assert_eq!( -// follower_1.read_with(cx, |editor, cx| editor.text(cx)), -// leader.read_with(cx, |editor, cx| editor.text(cx)) -// ); -// } - -// #[test] -// fn test_combine_syntax_and_fuzzy_match_highlights() { -// let string = "abcdefghijklmnop"; -// let syntax_ranges = [ -// ( -// 0..3, -// HighlightStyle { -// color: Some(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() -// }, -// ), -// ] -// ); -// } - -// #[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() -// }); -// }); - -// deterministic.run_until_parked(); - -// cx.update_editor(|editor, cx| { -// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); -// }); - -// cx.assert_editor_state(indoc! {" -// fn func(abc def: i32) -> ˇu32 { -// } -// "}); - -// cx.update_editor(|editor, cx| { -// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); -// }); - -// cx.assert_editor_state(indoc! {" -// fn func(abc ˇdef: i32) -> u32 { -// } -// "}); - -// cx.update_editor(|editor, cx| { -// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); -// }); - -// cx.assert_editor_state(indoc! {" -// fn func(abcˇ def: i32) -> u32 { -// } -// "}); - -// cx.update_editor(|editor, cx| { -// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); -// }); - -// cx.assert_editor_state(indoc! {" -// fn func(abc def: i32) -> ˇu32 { -// } -// "}); -// } - -// #[gpui::test] -// async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorTestContext::new(cx).await; - -// let diff_base = r#" -// use some::mod; - -// const A: u32 = 42; - -// fn main() { -// println!("hello"); - -// println!("world"); -// } -// "# -// .unindent(); - -// // Edits are modified, removed, modified, added -// cx.set_state( -// &r#" -// use some::modified; - -// ˇ -// fn main() { -// println!("hello there"); - -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); - -// cx.set_diff_base(Some(&diff_base)); -// deterministic.run_until_parked(); - -// cx.update_editor(|editor, cx| { -// //Wrap around the bottom of the buffer -// for _ in 0..3 { -// editor.go_to_hunk(&GoToHunk, cx); -// } -// }); - -// cx.assert_editor_state( -// &r#" -// ˇuse some::modified; - -// fn main() { -// println!("hello there"); - -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| { -// //Wrap around the top of the buffer -// for _ in 0..2 { -// editor.go_to_prev_hunk(&GoToPrevHunk, cx); -// } -// }); - -// cx.assert_editor_state( -// &r#" -// use some::modified; - -// fn main() { -// ˇ println!("hello there"); - -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| { -// editor.go_to_prev_hunk(&GoToPrevHunk, cx); -// }); - -// cx.assert_editor_state( -// &r#" -// use some::modified; - -// ˇ -// fn main() { -// println!("hello there"); - -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| { -// for _ in 0..3 { -// editor.go_to_prev_hunk(&GoToPrevHunk, cx); -// } -// }); - -// cx.assert_editor_state( -// &r#" -// use some::modified; - -// fn main() { -// ˇ println!("hello there"); - -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); - -// cx.update_editor(|editor, cx| { -// editor.fold(&Fold, cx); - -// //Make sure that the fold only gets one hunk -// for _ in 0..4 { -// editor.go_to_hunk(&GoToHunk, cx); -// } -// }); - -// cx.assert_editor_state( -// &r#" -// ˇuse some::modified; - -// fn main() { -// println!("hello there"); - -// println!("around the"); -// println!("world"); -// } -// "# -// .unindent(), -// ); -// } - -// #[test] -// fn test_split_words() { -// fn split<'a>(text: &'a str) -> Vec<&'a str> { -// split_words(text).collect() -// } - -// assert_eq!(split("HelloWorld"), &["Hello", "World"]); -// assert_eq!(split("hello_world"), &["hello_", "world"]); -// assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]); -// assert_eq!(split("Hello_World"), &["Hello_", "World"]); -// assert_eq!(split("helloWOrld"), &["hello", "WOrld"]); -// assert_eq!(split("helloworld"), &["helloworld"]); -// } - -// #[gpui::test] -// async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; -// let mut assert = |before, after| { -// let _state_context = cx.set_state(before); -// cx.update_editor(|editor, cx| { -// editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx) -// }); -// cx.assert_editor_state(after); -// }; - -// // Outside bracket jumps to outside of matching bracket -// assert("console.logˇ(var);", "console.log(var)ˇ;"); -// assert("console.log(var)ˇ;", "console.logˇ(var);"); - -// // Inside bracket jumps to inside of matching bracket -// assert("console.log(ˇvar);", "console.log(varˇ);"); -// assert("console.log(varˇ);", "console.log(ˇvar);"); - -// // When outside a bracket and inside, favor jumping to the inside bracket -// assert( -// "console.log('foo', [1, 2, 3]ˇ);", -// "console.log(ˇ'foo', [1, 2, 3]);", -// ); -// assert( -// "console.log(ˇ'foo', [1, 2, 3]);", -// "console.log('foo', [1, 2, 3]ˇ);", -// ); - -// // Bias forward if two options are equally likely -// assert( -// "let result = curried_fun()ˇ();", -// "let result = curried_fun()()ˇ;", -// ); - -// // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller -// assert( -// indoc! {" -// function test() { -// console.log('test')ˇ -// }"}, -// indoc! {" -// function test() { -// console.logˇ('test') -// }"}, -// ); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let (copilot, copilot_lsp) = Copilot::fake(cx); -// cx.update(|cx| cx.set_global(copilot)); -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string(), ":".to_string()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // When inserting, ensure autocompletion is favored over Copilot suggestions. -// cx.set_state(indoc! {" -// oneˇ -// two -// three -// "}); -// cx.simulate_keystroke("."); -// let _ = handle_completion_request( -// &mut cx, -// indoc! {" -// one.|<> -// two -// three -// "}, -// vec!["completion_a", "completion_b"], -// ); -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "one.copilot1".into(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), -// ..Default::default() -// }], -// vec![], -// ); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(editor.context_menu_visible()); -// assert!(!editor.has_active_copilot_suggestion(cx)); - -// // Confirming a completion inserts it and hides the context menu, without showing -// // the copilot suggestion afterwards. -// editor -// .confirm_completion(&Default::default(), cx) -// .unwrap() -// .detach(); -// assert!(!editor.context_menu_visible()); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); -// assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); -// }); - -// // Ensure Copilot suggestions are shown right away if no autocompletion is available. -// cx.set_state(indoc! {" -// oneˇ -// two -// three -// "}); -// cx.simulate_keystroke("."); -// let _ = handle_completion_request( -// &mut cx, -// indoc! {" -// one.|<> -// two -// three -// "}, -// vec![], -// ); -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "one.copilot1".into(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), -// ..Default::default() -// }], -// vec![], -// ); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(!editor.context_menu_visible()); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); -// }); - -// // Reset editor, and ensure autocompletion is still favored over Copilot suggestions. -// cx.set_state(indoc! {" -// oneˇ -// two -// three -// "}); -// cx.simulate_keystroke("."); -// let _ = handle_completion_request( -// &mut cx, -// indoc! {" -// one.|<> -// two -// three -// "}, -// vec!["completion_a", "completion_b"], -// ); -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "one.copilot1".into(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), -// ..Default::default() -// }], -// vec![], -// ); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(editor.context_menu_visible()); -// assert!(!editor.has_active_copilot_suggestion(cx)); - -// // When hiding the context menu, the Copilot suggestion becomes visible. -// editor.hide_context_menu(cx); -// assert!(!editor.context_menu_visible()); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); -// }); - -// // Ensure existing completion is interpolated when inserting again. -// cx.simulate_keystroke("c"); -// deterministic.run_until_parked(); -// cx.update_editor(|editor, cx| { -// assert!(!editor.context_menu_visible()); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); -// }); - -// // After debouncing, new Copilot completions should be requested. -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "one.copilot2".into(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), -// ..Default::default() -// }], -// vec![], -// ); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(!editor.context_menu_visible()); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); - -// // Canceling should remove the active Copilot suggestion. -// editor.cancel(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); - -// // After canceling, tabbing shouldn't insert the previously shown suggestion. -// editor.tab(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); - -// // When undoing the previously active suggestion is shown again. -// editor.undo(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); -// }); - -// // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. -// cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); -// cx.update_editor(|editor, cx| { -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); - -// // Tabbing when there is an active suggestion inserts it. -// editor.tab(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); - -// // When undoing the previously active suggestion is shown again. -// editor.undo(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); - -// // Hide suggestion. -// editor.cancel(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); -// }); - -// // If an edit occurs outside of this editor but no suggestion is being shown, -// // we won't make it visible. -// cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); -// cx.update_editor(|editor, cx| { -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); -// }); - -// // Reset the editor to verify how suggestions behave when tabbing on leading indentation. -// cx.update_editor(|editor, cx| { -// editor.set_text("fn foo() {\n \n}", cx); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 2)..Point::new(1, 2)]) -// }); -// }); -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: " let x = 4;".into(), -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), -// ..Default::default() -// }], -// vec![], -// ); - -// cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); -// assert_eq!(editor.text(cx), "fn foo() {\n \n}"); - -// // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. -// editor.tab(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.text(cx), "fn foo() {\n \n}"); -// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); - -// // Tabbing again accepts the suggestion. -// editor.tab(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); -// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); -// }); -// } - -// #[gpui::test] -// async fn test_copilot_completion_invalidation( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |_| {}); - -// let (copilot, copilot_lsp) = Copilot::fake(cx); -// cx.update(|cx| cx.set_global(copilot)); -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string(), ":".to_string()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.set_state(indoc! {" -// one -// twˇ -// three -// "}); - -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "two.foo()".into(), -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), -// ..Default::default() -// }], -// vec![], -// ); -// cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); -// assert_eq!(editor.text(cx), "one\ntw\nthree\n"); - -// editor.backspace(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); -// assert_eq!(editor.text(cx), "one\nt\nthree\n"); - -// editor.backspace(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); -// assert_eq!(editor.text(cx), "one\n\nthree\n"); - -// // Deleting across the original suggestion range invalidates it. -// editor.backspace(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one\nthree\n"); -// assert_eq!(editor.text(cx), "one\nthree\n"); - -// // Undoing the deletion restores the suggestion. -// editor.undo(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); -// assert_eq!(editor.text(cx), "one\n\nthree\n"); -// }); -// } - -// #[gpui::test] -// async fn test_copilot_multibuffer( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |_| {}); - -// let (copilot, copilot_lsp) = Copilot::fake(cx); -// cx.update(|cx| cx.set_global(copilot)); - -// let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); -// let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// buffer_1.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }], -// cx, -// ); -// multibuffer.push_excerpts( -// buffer_2.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }], -// cx, -// ); -// multibuffer -// }); -// let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); - -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "b = 2 + a".into(), -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)), -// ..Default::default() -// }], -// vec![], -// ); -// editor.update(cx, |editor, cx| { -// // Ensure copilot suggestions are shown for the first excerpt. -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 5)..Point::new(1, 5)]) -// }); -// editor.next_copilot_suggestion(&Default::default(), cx); -// }); -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// editor.update(cx, |editor, cx| { -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!( -// editor.display_text(cx), -// "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n" -// ); -// assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); -// }); - -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "d = 4 + c".into(), -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)), -// ..Default::default() -// }], -// vec![], -// ); -// editor.update(cx, |editor, cx| { -// // Move to another excerpt, ensuring the suggestion gets cleared. -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) -// }); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!( -// editor.display_text(cx), -// "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n" -// ); -// assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); - -// // Type a character, ensuring we don't even try to interpolate the previous suggestion. -// editor.handle_input(" ", cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!( -// editor.display_text(cx), -// "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n" -// ); -// assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); -// }); - -// // Ensure the new suggestion is displayed when the debounce timeout expires. -// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// editor.update(cx, |editor, cx| { -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!( -// editor.display_text(cx), -// "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n" -// ); -// assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); -// }); -// } - -// #[gpui::test] -// async fn test_copilot_disabled_globs( -// deterministic: Arc, -// 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); -// }); - -// cx.foreground().run_until_parked(); - -// buffer.read_with(cx, |buffer, _| { -// assert_eq!( -// buffer.text(), -// "fn main() { let a = {5}; }", -// "No extra braces from on type formatting should appear in the buffer" -// ) -// }); -// } - -// #[gpui::test] -// async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let language_name: Arc = "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.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { let a = 5; }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// let _fake_server = fake_servers.next().await.unwrap(); -// update_test_language_settings(cx, |language_settings| { -// language_settings.languages.insert( -// Arc::clone(&language_name), -// LanguageSettingsContent { -// tab_size: NonZeroU32::new(8), -// ..Default::default() -// }, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// server_restarts.load(atomic::Ordering::Acquire), -// 0, -// "Should not restart LSP server on an unrelated change" -// ); - -// update_test_project_settings(cx, |project_settings| { -// project_settings.lsp.insert( -// "Some other server name".into(), -// LspSettings { -// initialization_options: Some(json!({ -// "some other init value": false -// })), -// }, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// server_restarts.load(atomic::Ordering::Acquire), -// 0, -// "Should not restart LSP server on an unrelated LSP settings change" -// ); - -// update_test_project_settings(cx, |project_settings| { -// project_settings.lsp.insert( -// language_server_name.into(), -// LspSettings { -// initialization_options: Some(json!({ -// "anotherInitValue": false -// })), -// }, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// server_restarts.load(atomic::Ordering::Acquire), -// 1, -// "Should restart LSP server on a related LSP settings change" -// ); - -// update_test_project_settings(cx, |project_settings| { -// project_settings.lsp.insert( -// language_server_name.into(), -// LspSettings { -// initialization_options: Some(json!({ -// "anotherInitValue": false -// })), -// }, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// server_restarts.load(atomic::Ordering::Acquire), -// 1, -// "Should not restart LSP server on a related LSP settings change that is the same" -// ); - -// update_test_project_settings(cx, |project_settings| { -// project_settings.lsp.insert( -// language_server_name.into(), -// LspSettings { -// initialization_options: None, -// }, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// server_restarts.load(atomic::Ordering::Acquire), -// 2, -// "Should restart LSP server on another related LSP settings change" -// ); -// } - -// #[gpui::test] -// async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string()]), -// resolve_provider: Some(true), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); -// cx.simulate_keystroke("."); -// let completion_item = lsp::CompletionItem { -// label: "some".into(), -// kind: Some(lsp::CompletionItemKind::SNIPPET), -// detail: Some("Wrap the expression in an `Option::Some`".to_string()), -// documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "```rust\nSome(2)\n```".to_string(), -// })), -// deprecated: Some(false), -// sort_text: Some("fffffff2".to_string()), -// filter_text: Some("some".to_string()), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// range: lsp::Range { -// start: lsp::Position { -// line: 0, -// character: 22, -// }, -// end: lsp::Position { -// line: 0, -// character: 22, -// }, -// }, -// new_text: "Some(2)".to_string(), -// })), -// additional_text_edits: Some(vec![lsp::TextEdit { -// range: lsp::Range { -// start: lsp::Position { -// line: 0, -// character: 20, -// }, -// end: lsp::Position { -// line: 0, -// character: 22, -// }, -// }, -// new_text: "".to_string(), -// }]), -// ..Default::default() -// }; - -// let closure_completion_item = completion_item.clone(); -// let mut request = cx.handle_request::(move |_, _, _| { -// let task_completion_item = closure_completion_item.clone(); -// async move { -// Ok(Some(lsp::CompletionResponse::Array(vec![ -// task_completion_item, -// ]))) -// } -// }); - -// request.next().await; - -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"}); - -// cx.handle_request::(move |_, _, _| { -// let task_completion_item = completion_item.clone(); -// async move { Ok(task_completion_item) } -// }) -// .next() -// .await -// .unwrap(); -// apply_additional_edits.await.unwrap(); -// cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); -// } - -// #[gpui::test] -// async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new( -// Language::new( -// LanguageConfig { -// path_suffixes: vec!["jsx".into()], -// overrides: [( -// "element".into(), -// LanguageConfigOverride { -// word_characters: Override::Set(['-'].into_iter().collect()), -// ..Default::default() -// }, -// )] -// .into_iter() -// .collect(), -// ..Default::default() -// }, -// Some(tree_sitter_typescript::language_tsx()), -// ) -// .with_override_query("(jsx_self_closing_element) @element") -// .unwrap(), -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![":".to_string()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.lsp -// .handle_request::(move |_, _| async move { -// Ok(Some(lsp::CompletionResponse::Array(vec![ -// lsp::CompletionItem { -// label: "bg-blue".into(), -// ..Default::default() -// }, -// lsp::CompletionItem { -// label: "bg-red".into(), -// ..Default::default() -// }, -// lsp::CompletionItem { -// label: "bg-yellow".into(), -// ..Default::default() -// }, -// ]))) -// }); - -// cx.set_state(r#"

"#); - -// // Trigger completion when typing a dash, because the dash is an extra -// // word character in the 'element' scope, which contains the cursor. -// cx.simulate_keystroke("-"); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, _| { -// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { -// assert_eq!( -// menu.matches.iter().map(|m| &m.string).collect::>(), -// &["bg-red", "bg-blue", "bg-yellow"] -// ); -// } else { -// panic!("expected completion menu to be open"); -// } -// }); - -// cx.simulate_keystroke("l"); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, _| { -// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { -// assert_eq!( -// menu.matches.iter().map(|m| &m.string).collect::>(), -// &["bg-blue", "bg-yellow"] -// ); -// } else { -// panic!("expected completion menu to be open"); -// } -// }); - -// // When filtering completions, consider the character after the '-' to -// // be the start of a subword. -// cx.set_state(r#"

"#); -// cx.simulate_keystroke("l"); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, _| { -// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { -// assert_eq!( -// menu.matches.iter().map(|m| &m.string).collect::>(), -// &["bg-yellow"] -// ); -// } else { -// panic!("expected completion menu to be open"); -// } -// }); -// } - -// #[gpui::test] -// async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.formatter = Some(language_settings::Formatter::Prettier) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// prettier_parser_name: Some("test_parser".to_string()), -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); - -// let test_plugin = "test_plugin"; -// let _ = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// prettier_plugins: vec![test_plugin], -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_file("/file.rs", Default::default()).await; - -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; -// project.update(cx, |project, _| { -// project.languages().add(Arc::new(language)); -// }); -// let buffer = project -// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) -// .await -// .unwrap(); - -// let buffer_text = "one\ntwo\nthree\n"; -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); -// editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); - -// let format = editor.update(cx, |editor, cx| { -// editor.perform_format(project.clone(), FormatTrigger::Manual, cx) -// }); -// format.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// buffer_text.to_string() + prettier_format_suffix, -// "Test prettier formatting was not applied to the original buffer text", -// ); - -// update_test_language_settings(cx, |settings| { -// settings.defaults.formatter = Some(language_settings::Formatter::Auto) -// }); -// let format = editor.update(cx, |editor, cx| { -// editor.perform_format(project.clone(), FormatTrigger::Manual, cx) -// }); -// format.await.unwrap(); -// assert_eq!( -// editor.read_with(cx, |editor, cx| editor.text(cx)), -// buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, -// "Autoformatting (via test prettier) was not applied to the original buffer text", -// ); -// } - -// fn empty_range(row: usize, column: usize) -> Range { -// let point = DisplayPoint::new(row as u32, column as u32); -// point..point -// } - -// fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext) { -// let (text, ranges) = marked_text_ranges(marked_text, true); -// assert_eq!(view.text(cx), text); -// assert_eq!( -// view.selections.ranges(cx), -// ranges, -// "Assert selections are {}", -// marked_text -// ); -// } - -// /// Handle completion request passing a marked string specifying where the completion -// /// should be triggered from using '|' character, what range should be replaced, and what completions -// /// should be returned using '<' and '>' to delimit the range -// pub fn handle_completion_request<'a>( -// cx: &mut EditorLspTestContext<'a>, -// marked_string: &str, -// completions: Vec<&'static str>, -// ) -> impl Future { -// 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; -// } -// } - -// 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() -// }) -// } -// }); - -// 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(), -// }) -// } -// }); -// } - -// pub(crate) fn update_test_language_settings( -// cx: &mut TestAppContext, -// f: impl Fn(&mut AllLanguageSettingsContent), -// ) { -// cx.update(|cx| { -// cx.update_global::(|store, 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(); - -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init((), cx); -// client::init_settings(cx); -// language::init(cx); -// Project::init_settings(cx); -// workspace::init_settings(cx); -// crate::init(cx); -// }); - -// update_test_language_settings(cx, f); -// } +use super::*; +use crate::{ + scroll::scroll_amount::ScrollAmount, + test::{ + assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, + editor_test_context::EditorTestContext, select_ranges, + }, + JoinLines, +}; +use drag_and_drop::DragAndDrop; +use futures::StreamExt; +use gpui::{ + executor::Deterministic, + geometry::{rect::RectF, vector::vec2f}, + platform::{WindowBounds, WindowOptions}, + serde_json::{self, json}, + TestAppContext, +}; +use indoc::indoc; +use language::{ + language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, + BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, + Override, Point, +}; +use parking_lot::Mutex; +use project::project_settings::{LspSettings, ProjectSettings}; +use project::FakeFs; +use std::sync::atomic; +use std::sync::atomic::AtomicUsize; +use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; +use unindent::Unindent; +use util::{ + assert_set_eq, + test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, +}; +use workspace::{ + item::{FollowableItem, Item, ItemHandle}, + NavigationEntry, ViewId, +}; + +#[gpui::test] +fn test_edit_events(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.add_model(|cx| { + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456"); + buffer.set_group_interval(Duration::from_secs(1)); + buffer + }); + + let events = Rc::new(RefCell::new(Vec::new())); + let editor1 = cx + .add_window({ + let events = events.clone(); + |cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!( + event, + Event::Edited | Event::BufferEdited | Event::DirtyChanged + ) { + events.borrow_mut().push(("editor1", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }) + .root(cx); + let editor2 = cx + .add_window({ + let events = events.clone(); + |cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!( + event, + Event::Edited | Event::BufferEdited | Event::DirtyChanged + ) { + events.borrow_mut().push(("editor2", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }) + .root(cx); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); + + // Mutating editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.insert("X", cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::DirtyChanged), + ("editor2", Event::DirtyChanged) + ] + ); + + // Mutating editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // Undoing on editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::DirtyChanged), + ("editor2", Event::DirtyChanged), + ] + ); + + // Redoing on editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::DirtyChanged), + ("editor2", Event::DirtyChanged), + ] + ); + + // Undoing on editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::DirtyChanged), + ("editor2", Event::DirtyChanged), + ] + ); + + // Redoing on editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::DirtyChanged), + ("editor2", Event::DirtyChanged), + ] + ); + + // No event is emitted when the mutation is a no-op. + editor2.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([0..0])); + + editor.backspace(&Backspace, cx); + }); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); +} + +#[gpui::test] +fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut now = Instant::now(); + let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456")); + let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx + .add_window(|cx| build_editor(buffer.clone(), cx)) + .root(cx); + + editor.update(cx, |editor, cx| { + editor.start_transaction_at(now, cx); + editor.change_selections(None, cx, |s| s.select_ranges([2..4])); + + editor.insert("cd", cx); + editor.end_transaction_at(now, cx); + assert_eq!(editor.text(cx), "12cd56"); + assert_eq!(editor.selections.ranges(cx), vec![4..4]); + + editor.start_transaction_at(now, cx); + editor.change_selections(None, cx, |s| s.select_ranges([4..5])); + editor.insert("e", cx); + editor.end_transaction_at(now, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selections.ranges(cx), vec![5..5]); + + now += group_interval + Duration::from_millis(1); + editor.change_selections(None, cx, |s| s.select_ranges([2..2])); + + // Simulate an edit in another editor + buffer.update(cx, |buffer, cx| { + buffer.start_transaction_at(now, cx); + buffer.edit([(0..1, "a")], None, cx); + buffer.edit([(1..1, "b")], None, cx); + buffer.end_transaction_at(now, cx); + }); + + assert_eq!(editor.text(cx), "ab2cde6"); + assert_eq!(editor.selections.ranges(cx), vec![3..3]); + + // Last transaction happened past the group interval in a different editor. + // Undo it individually and don't restore selections. + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selections.ranges(cx), vec![2..2]); + + // First two transactions happened within the group interval in this editor. + // Undo them together and restore selections. + editor.undo(&Undo, cx); + editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. + assert_eq!(editor.text(cx), "123456"); + assert_eq!(editor.selections.ranges(cx), vec![0..0]); + + // Redo the first two transactions together. + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selections.ranges(cx), vec![5..5]); + + // Redo the last transaction on its own. + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "ab2cde6"); + assert_eq!(editor.selections.ranges(cx), vec![6..6]); + + // Test empty transactions. + editor.start_transaction_at(now, cx); + editor.end_transaction_at(now, cx); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "12cde6"); + }); +} + +#[gpui::test] +fn test_ime_composition(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.add_model(|cx| { + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde"); + // Ensure automatic grouping doesn't occur. + buffer.set_group_interval(Duration::ZERO); + buffer + }); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.add_window(|cx| { + let mut editor = build_editor(buffer.clone(), cx); + + // Start a new IME composition. + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); + assert_eq!(editor.text(cx), "äbcde"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) + ); + + // Finalize IME composition. + editor.replace_text_in_range(None, "ā", cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // IME composition edits are grouped and are undone/redone at once. + editor.undo(&Default::default(), cx); + assert_eq!(editor.text(cx), "abcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + editor.redo(&Default::default(), cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition. + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) + ); + + // Undoing during an IME composition cancels it. + editor.undo(&Default::default(), cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition with an invalid marked range, ensuring it gets clipped. + editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); + assert_eq!(editor.text(cx), "ābcdè"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(4)..OffsetUtf16(5)]) + ); + + // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. + editor.replace_text_in_range(Some(4..999), "ę", cx); + assert_eq!(editor.text(cx), "ābcdę"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition with multiple cursors. + editor.change_selections(None, cx, |s| { + s.select_ranges([ + OffsetUtf16(1)..OffsetUtf16(1), + OffsetUtf16(3)..OffsetUtf16(3), + OffsetUtf16(5)..OffsetUtf16(5), + ]) + }); + editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); + assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![ + OffsetUtf16(0)..OffsetUtf16(3), + OffsetUtf16(4)..OffsetUtf16(7), + OffsetUtf16(8)..OffsetUtf16(11) + ]) + ); + + // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. + editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); + assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![ + OffsetUtf16(1)..OffsetUtf16(2), + OffsetUtf16(5)..OffsetUtf16(6), + OffsetUtf16(9)..OffsetUtf16(10) + ]) + ); + + // Finalize IME composition with multiple cursors. + editor.replace_text_in_range(Some(9..10), "2", cx); + assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); + assert_eq!(editor.marked_text_ranges(cx), None); + + editor + }); +} + +#[gpui::test] +fn test_selection_with_mouse(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + editor.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); + }); + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); + + editor.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + + editor.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + editor.update(cx, |view, cx| { + view.end_selection(cx); + view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + editor.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); + view.update_selection(DisplayPoint::new(0, 0), 0, Point::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [ + DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) + ] + ); + + editor.update(cx, |view, cx| { + view.end_selection(cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] + ); +} + +#[gpui::test] +fn test_canceling_pending_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); + }); + + view.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + }); +} + +#[gpui::test] +fn test_clone(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let (text, selection_ranges) = marked_text_ranges( + indoc! {" + one + two + threeˇ + four + fiveˇ + "}, + true, + ); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&text, cx); + build_editor(buffer, cx) + }) + .root(cx); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); + editor.fold_ranges( + [ + Point::new(1, 0)..Point::new(2, 0), + Point::new(3, 0)..Point::new(4, 0), + ], + true, + cx, + ); + }); + + let cloned_editor = editor + .update(cx, |editor, cx| { + cx.add_window(Default::default(), |cx| editor.clone(cx)) + }) + .root(cx); + + let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); + let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); + + assert_eq!( + cloned_editor.update(cx, |e, cx| e.display_text(cx)), + editor.update(cx, |e, cx| e.display_text(cx)) + ); + assert_eq!( + cloned_snapshot + .folds_in_range(0..text.len()) + .collect::>(), + 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)) + ); +} + +#[gpui::test] +async fn test_navigation_history(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.set_global(DragAndDrop::::default()); + use workspace::item::Item; + + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, [], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + window.add_view(cx, |cx| { + let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); + let mut editor = build_editor(buffer.clone(), cx); + let handle = cx.handle(); + editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); + + fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { + editor.nav_history.as_mut().unwrap().pop_backward(cx) + } + + // Move the cursor a small distance. + // Nothing is added to the navigation history. + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) + }); + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) + }); + assert!(pop_history(&mut editor, cx).is_none()); + + // Move the cursor a large distance. + // The history can jump back to the previous position. + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)]) + }); + let nav_entry = pop_history(&mut editor, cx).unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item.id(), cx.view_id()); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); + + // Move the cursor a small distance via the mouse. + // Nothing is added to the navigation history. + editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); + + // Move the cursor a large distance via the mouse. + // The history can jump back to the previous position. + editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] + ); + let nav_entry = pop_history(&mut editor, cx).unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item.id(), cx.view_id()); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); + + // Set scroll position to check later + editor.set_scroll_position(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)] + ); + assert_eq!( + editor.scroll_position(cx), + vec2f(0., editor.max_point(cx).row() as f32) + ); + + editor + }); +} + +#[gpui::test] +fn test_cancel(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); + view.update_selection(DisplayPoint::new(1, 1), 0, 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); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), + ] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] + ); + }); +} + +#[gpui::test] +fn test_fold_action(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple( + &" + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() { + 2 + } + + fn c() { + 3 + } + } + " + .unindent(), + cx, + ); + build_editor(buffer.clone(), cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]); + }); + view.fold(&Fold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {⋯ + } + + fn c() {⋯ + } + } + " + .unindent(), + ); + + view.fold(&Fold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo {⋯ + } + " + .unindent(), + ); + + view.unfold_lines(&UnfoldLines, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {⋯ + } + + fn c() {⋯ + } + } + " + .unindent(), + ); + + view.unfold_lines(&UnfoldLines, cx); + assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); + }); +} + +#[gpui::test] +fn test_move_cursor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); + let view = cx + .add_window(|cx| build_editor(buffer.clone(), cx)) + .root(cx); + + buffer.update(cx, |buffer, cx| { + buffer.edit( + vec![ + (Point::new(1, 0)..Point::new(1, 0), "\t"), + (Point::new(1, 1)..Point::new(1, 1), "\t"), + ], + None, + cx, + ); + }); + view.update(cx, |view, cx| { + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] + ); + + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.move_to_end(&MoveToEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] + ); + + view.move_to_beginning(&MoveToBeginning, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]); + }); + view.select_to_beginning(&SelectToBeginning, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] + ); + + view.select_to_end(&SelectToEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] + ); + }); +} + +#[gpui::test] +fn test_move_cursor_multibyte(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); + build_editor(buffer.clone(), cx) + }) + .root(cx); + + assert_eq!('ⓐ'.len_utf8(), 3); + assert_eq!('α'.len_utf8(), 2); + + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 6)..Point::new(0, 12), + Point::new(1, 2)..Point::new(1, 4), + Point::new(2, 4)..Point::new(2, 8), + ], + true, + cx, + ); + assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε"); + + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐⓑ⋯".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "a".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "α".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ⋯".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ⋯ε".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ⋯ε".len())] + ); + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐ".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "".len())] + ); + }); +} + +#[gpui::test] +fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); + build_editor(buffer.clone(), cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); + }); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "abcd".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβγ".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(3, "abcd".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(3, "abcd".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβγ".len())] + ); + }); +} + +#[gpui::test] +fn test_beginning_end_of_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\n def", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ]); + }); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_end_of_line(&MoveToEndOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + }); + + // Moving to the end of line again is a no-op. + view.update(cx, |view, cx| { + view.move_to_end_of_line(&MoveToEndOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_left(&MoveLeft, cx); + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_end_of_line( + &SelectToEndOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.delete_to_end_of_line(&DeleteToEndOfLine, cx); + assert_eq!(view.display_text(cx), "ab\n de"); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ] + ); + }); + + view.update(cx, |view, cx| { + view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); + assert_eq!(view.display_text(cx), "\n"); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); +} + +#[gpui::test] +fn test_prev_next_word_boundary(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), + ]) + }); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); + + view.move_right(&MoveRight, cx); + view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); + assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); + + view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); + assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx); + + view.select_to_next_word_end(&SelectToNextWordEnd, cx); + assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); + }); +} + +#[gpui::test] +fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = + MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); + build_editor(buffer, cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.set_wrap_width(Some(140.), cx); + assert_eq!( + view.display_text(cx), + "use one::{\n two::three::\n four::five\n};" + ); + + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]); + }); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] + ); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] + ); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + ); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] + ); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + ); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] + ); + }); +} + +#[gpui::test] +async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); + let window = cx.window; + window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); + + cx.set_state( + &r#"ˇone + two + + three + fourˇ + five + + six"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + ˇ + three + four + five + ˇ + six"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + + three + four + five + ˇ + sixˇ"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + + three + four + five + + sixˇ"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + + three + four + five + ˇ + six"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + ˇ + three + four + five + + six"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"ˇone + two + + three + four + five + + six"# + .unindent(), + ); +} + +#[gpui::test] +async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); + let window = cx.window; + window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx); + + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); + + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)); + editor.scroll_screen(&ScrollAmount::Page(1.), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + editor.scroll_screen(&ScrollAmount::Page(1.), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.)); + editor.scroll_screen(&ScrollAmount::Page(-1.), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.)); + editor.scroll_screen(&ScrollAmount::Page(0.5), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + }); +} + +#[gpui::test] +async fn test_autoscroll(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.update_editor(|editor, cx| { + editor.set_vertical_scroll_margin(2, cx); + editor.style(cx).text.line_height(cx.font_cache()) + }); + + let window = cx.window; + window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx); + + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0)); + }); + + // Add a cursor below the visible area. Since both cursors cannot fit + // on screen, the editor autoscrolls to reveal the newest cursor, and + // allows the vertical scroll margin below that cursor. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(0, 0)..Point::new(0, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0)); + }); + + // Move down. The editor cursor scrolls down to track the newest cursor. + cx.update_editor(|editor, cx| { + editor.move_down(&Default::default(), cx); + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0)); + }); + + // Add a cursor above the visible area. Since both cursors fit on screen, + // the editor scrolls to show both. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(1, 0)..Point::new(1, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0)); + }); +} + +#[gpui::test] +async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); + let window = cx.window; + window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); + + cx.set_state( + &r#" + ˇone + two + threeˇ + four + five + six + seven + eight + nine + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); + cx.assert_editor_state( + &r#" + one + two + three + ˇfour + five + sixˇ + seven + eight + nine + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); + cx.assert_editor_state( + &r#" + one + two + three + four + five + six + ˇseven + eight + nineˇ + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); + cx.assert_editor_state( + &r#" + one + two + three + ˇfour + five + sixˇ + seven + eight + nine + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); + cx.assert_editor_state( + &r#" + ˇone + two + threeˇ + four + five + six + seven + eight + nine + ten + "# + .unindent(), + ); + + // Test select collapsing + cx.update_editor(|editor, cx| { + editor.move_page_down(&MovePageDown::default(), cx); + editor.move_page_down(&MovePageDown::default(), cx); + editor.move_page_down(&MovePageDown::default(), cx); + }); + cx.assert_editor_state( + &r#" + one + two + three + four + five + six + seven + eight + nine + ˇten + ˇ"# + .unindent(), + ); +} + +#[gpui::test] +async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("one «two threeˇ» four"); + cx.update_editor(|editor, cx| { + editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); + assert_eq!(editor.text(cx), " four"); + }); +} + +#[gpui::test] +fn test_delete_to_word_boundary(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("one two three four", cx); + build_editor(buffer.clone(), cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + // an empty selection - the preceding word fragment is deleted + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + // characters selected - they are deleted + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12), + ]) + }); + view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx); + assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four"); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + // an empty selection - the following word fragment is deleted + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + // characters selected - they are deleted + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10), + ]) + }); + view.delete_to_next_word_end(&DeleteToNextWordEnd, cx); + assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our"); + }); +} + +#[gpui::test] +fn test_newline(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); + build_editor(buffer.clone(), cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), + ]) + }); + + view.newline(&Newline, cx); + assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); + }); +} + +#[gpui::test] +fn test_newline_with_old_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple( + " + a + b( + X + ) + c( + X + ) + " + .unindent() + .as_str(), + cx, + ); + let mut editor = build_editor(buffer.clone(), cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(2, 4)..Point::new(2, 5), + Point::new(5, 4)..Point::new(5, 5), + ]) + }); + editor + }) + .root(cx); + + editor.update(cx, |editor, cx| { + // Edit the buffer directly, deleting ranges surrounding the editor's selections + editor.buffer.update(cx, |buffer, cx| { + buffer.edit( + [ + (Point::new(1, 2)..Point::new(3, 0), ""), + (Point::new(4, 2)..Point::new(6, 0), ""), + ], + None, + cx, + ); + assert_eq!( + buffer.read(cx).text(), + " + a + b() + c() + " + .unindent() + ); + }); + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(1, 2)..Point::new(1, 2), + Point::new(2, 2)..Point::new(2, 2), + ], + ); + + editor.newline(&Newline, cx); + assert_eq!( + editor.text(cx), + " + a + b( + ) + c( + ) + " + .unindent() + ); + + // The selections are moved after the inserted newlines + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(2, 0)..Point::new(2, 0), + Point::new(4, 0)..Point::new(4, 0), + ], + ); + }); +} + +#[gpui::test] +async fn test_newline_above(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "(" ")" @end) @indent"#) + .unwrap(), + ); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + const a: ˇA = ( + (ˇ + «const_functionˇ»(ˇ), + so«mˇ»et«hˇ»ing_ˇelse,ˇ + )ˇ + ˇ);ˇ + "}); + + cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); + cx.assert_editor_state(indoc! {" + ˇ + const a: A = ( + ˇ + ( + ˇ + ˇ + const_function(), + ˇ + ˇ + ˇ + ˇ + something_else, + ˇ + ) + ˇ + ˇ + ); + "}); +} + +#[gpui::test] +async fn test_newline_below(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "(" ")" @end) @indent"#) + .unwrap(), + ); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + const a: ˇA = ( + (ˇ + «const_functionˇ»(ˇ), + so«mˇ»et«hˇ»ing_ˇelse,ˇ + )ˇ + ˇ);ˇ + "}); + + cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); + cx.assert_editor_state(indoc! {" + const a: A = ( + ˇ + ( + ˇ + const_function(), + ˇ + ˇ + something_else, + ˇ + ˇ + ˇ + ˇ + ) + ˇ + ); + ˇ + ˇ + "}); +} + +#[gpui::test] +async fn test_newline_comments(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("//".into()), + ..LanguageConfig::default() + }, + None, + )); + { + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + // Fooˇ + "}); + + cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.assert_editor_state(indoc! {" + // Foo + //ˇ + "}); + // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix. + cx.set_state(indoc! {" + ˇ// Foo + "}); + cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.assert_editor_state(indoc! {" + + ˇ// Foo + "}); + } + // Ensure that comment continuations can be disabled. + update_test_language_settings(cx, |settings| { + settings.defaults.extend_comment_on_newline = Some(false); + }); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + // Fooˇ + "}); + cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.assert_editor_state(indoc! {" + // Foo + ˇ + "}); +} + +#[gpui::test] +fn test_insert_with_old_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); + let mut editor = build_editor(buffer.clone(), cx); + editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); + editor + }) + .root(cx); + + editor.update(cx, |editor, cx| { + // Edit the buffer directly, deleting ranges surrounding the editor's selections + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); + assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); + }); + assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],); + + editor.insert("Z", cx); + assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); + + // The selections are moved after the inserted characters + assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],); + }); +} + +#[gpui::test] +async fn test_tab(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(3) + }); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + ˇabˇc + ˇ🏀ˇ🏀ˇefg + dˇ + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + ˇab ˇc + ˇ🏀 ˇ🏀 ˇefg + d ˇ + "}); + + cx.set_state(indoc! {" + a + «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + a + «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» + "}); +} + +#[gpui::test] +async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "(" ")" @end) @indent"#) + .unwrap(), + ); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // cursors that are already at the suggested indent level insert + // a soft tab. cursors that are to the left of the suggested indent + // auto-indent their line. + cx.set_state(indoc! {" + ˇ + const a: B = ( + c( + d( + ˇ + ) + ˇ + ˇ ) + ); + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + ˇ + const a: B = ( + c( + d( + ˇ + ) + ˇ + ˇ) + ); + "}); + + // handle auto-indent when there are multiple cursors on the same line + cx.set_state(indoc! {" + const a: B = ( + c( + ˇ ˇ + ˇ ) + ); + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c( + ˇ + ˇ) + ); + "}); +} + +#[gpui::test] +async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "{" "}" @end) @indent"#) + .unwrap(), + ); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + fn a() { + if b { + \t ˇc + } + } + "}); + + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + fn a() { + if b { + ˇc + } + } + "}); +} + +#[gpui::test] +async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4); + }); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + + // select across line ending + cx.set_state(indoc! {" + one two + t«hree + ˇ» four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + t«hree + ˇ» four + "}); + + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + t«hree + ˇ» four + "}); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + cx.set_state(indoc! {" + one two + ˇthree + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); + + cx.set_state(indoc! {" + one two + ˇ three + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); +} + +#[gpui::test] +async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.hard_tabs = Some(true); + }); + + let mut cx = EditorTestContext::new(cx).await; + + // select two ranges on one line + cx.set_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + \t«oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + \t\t«oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + \t«oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + + // select across a line ending + cx.set_state(indoc! {" + one two + t«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \tt«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \t\tt«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + \tt«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + t«hree + ˇ»four + "}); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + cx.set_state(indoc! {" + one two + ˇthree + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \tˇthree + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); +} + +#[gpui::test] +fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.languages.extend([ + ( + "TOML".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(2), + ..Default::default() + }, + ), + ( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(4), + ..Default::default() + }, + ), + ]); + }); + + let toml_language = Arc::new(Language::new( + LanguageConfig { + name: "TOML".into(), + ..Default::default() + }, + None, + )); + let rust_language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + ..Default::default() + }, + None, + )); + + let toml_buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx) + }); + let rust_buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") + .with_language(rust_language, cx) + }); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + toml_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + rust_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer + }); + + cx.add_window(|cx| { + let mut editor = build_editor(multibuffer, cx); + + assert_eq!( + editor.text(cx), + indoc! {" + a = 1 + b = 2 + + const c: usize = 3; + "} + ); + + select_ranges( + &mut editor, + indoc! {" + «aˇ» = 1 + b = 2 + + «const c:ˇ» usize = 3; + "}, + cx, + ); + + editor.tab(&Tab, cx); + assert_text_with_selections( + &mut editor, + indoc! {" + «aˇ» = 1 + b = 2 + + «const c:ˇ» usize = 3; + "}, + cx, + ); + editor.tab_prev(&TabPrev, cx); + assert_text_with_selections( + &mut editor, + indoc! {" + «aˇ» = 1 + b = 2 + + «const c:ˇ» usize = 3; + "}, + cx, + ); + + editor + }); +} + +#[gpui::test] +async fn test_backspace(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Basic backspace + cx.set_state(indoc! {" + onˇe two three + fou«rˇ» five six + seven «ˇeight nine + »ten + "}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + oˇe two three + fouˇ five six + seven ˇten + "}); + + // Test backspace inside and around indents + cx.set_state(indoc! {" + zero + ˇone + ˇtwo + ˇ ˇ ˇ three + ˇ ˇ four + "}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + zero + ˇone + ˇtwo + ˇ threeˇ four + "}); + + // Test backspace with line_mode set to true + cx.update_editor(|e, _| e.selections.line_mode = true); + cx.set_state(indoc! {" + The ˇquick ˇbrown + fox jumps over + the lazy dog + ˇThe qu«ick bˇ»rown"}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + ˇfox jumps over + the lazy dogˇ"}); +} + +#[gpui::test] +async fn test_delete(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + onˇe two three + fou«rˇ» five six + seven «ˇeight nine + »ten + "}); + cx.update_editor(|e, cx| e.delete(&Delete, cx)); + cx.assert_editor_state(indoc! {" + onˇ two three + fouˇ five six + seven ˇten + "}); + + // Test backspace with line_mode set to true + cx.update_editor(|e, _| e.selections.line_mode = true); + cx.set_state(indoc! {" + The ˇquick ˇbrown + fox «ˇjum»ps over + the lazy dog + ˇThe qu«ick bˇ»rown"}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state("ˇthe lazy dogˇ"); +} + +#[gpui::test] +fn test_delete_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ]) + }); + view.delete_line(&DeleteLine, cx); + assert_eq!(view.display_text(cx), "ghi"); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) + ] + ); + }); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) + }); + view.delete_line(&DeleteLine, cx); + assert_eq!(view.display_text(cx), "ghi\n"); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] + ); + }); +} + +#[gpui::test] +fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); + let mut editor = build_editor(buffer.clone(), cx); + let buffer = buffer.read(cx).as_singleton().unwrap(); + + assert_eq!( + editor.selections.ranges::(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), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_line(&SelectLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] + ); + }); +} + +#[gpui::test] +fn test_split_selection_into_lines(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + true, + cx, + ); + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ]) + }); + assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"); + }); + + view.update(cx, |view, cx| { + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) + ] + ); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)]) + }); + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), + DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), + DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), + DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), + DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) + ] + ); + }); +} + +#[gpui::test] +fn test_add_selection_above_below(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]) + }); + }); + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); + + view.undo_selection(&UndoSelection, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + + view.redo_selection(&RedoSelection, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]) + }); + }); + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)]) + }); + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)]) + }); + }); + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); + }); +} + +#[gpui::test] +async fn test_select_next(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); +} + +#[gpui::test] +async fn test_select_previous(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + { + // `Select previous` without a selection (selects wordwise) + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); + } + { + // `Select previous` with a selection + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); + } +} + +#[gpui::test] +async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + )); + + let text = r#" + use mod1::mod2::{mod3, mod4}; + + fn fn_1(param1: bool, param2: &str) { + let var1 = "text"; + } + "# + .unindent(); + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ]); + }); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| { view.selections.display_ranges(cx) }), + &[ + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), + ] + ); + + view.update(cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), + ] + ); + + view.update(cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] + ); + + // Trying to expand the selected syntax node one more time has no effect. + view.update(cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] + ); + + view.update(cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), + ] + ); + + view.update(cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), + ] + ); + + view.update(cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ] + ); + + // Trying to shrink the selected syntax node one more time has no effect. + view.update(cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ] + ); + + // Ensure that we keep expanding the selection if the larger selection starts or ends within + // a fold. + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 21)..Point::new(0, 24), + Point::new(3, 20)..Point::new(3, 22), + ], + true, + cx, + ); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), + ] + ); +} + +#[gpui::test] +async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let text = "fn a() {}"; + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor + .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); + editor.newline(&Newline, cx); + assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(1, 4)..Point::new(1, 4), + Point::new(3, 4)..Point::new(3, 4), + Point::new(5, 0)..Point::new(5, 0) + ] + ); + }); +} + +#[gpui::test] +async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/*".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "[".to_string(), + end: "]".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "\"".to_string(), + end: "\"".to_string(), + close: true, + newline: false, + }, + ], + ..Default::default() + }, + autoclose_before: "})]".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(language.clone()); + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(language), cx); + }); + + cx.set_state( + &r#" + 🏀ˇ + εˇ + ❤️ˇ + "# + .unindent(), + ); + + // autoclose multiple nested brackets at multiple cursors + cx.update_editor(|view, cx| { + view.handle_input("{", cx); + view.handle_input("{", cx); + view.handle_input("{", cx); + }); + cx.assert_editor_state( + &" + 🏀{{{ˇ}}} + ε{{{ˇ}}} + ❤️{{{ˇ}}} + " + .unindent(), + ); + + // insert a different closing bracket + cx.update_editor(|view, cx| { + view.handle_input(")", cx); + }); + cx.assert_editor_state( + &" + 🏀{{{)ˇ}}} + ε{{{)ˇ}}} + ❤️{{{)ˇ}}} + " + .unindent(), + ); + + // skip over the auto-closed brackets when typing a closing bracket + cx.update_editor(|view, cx| { + view.move_right(&MoveRight, cx); + view.handle_input("}", cx); + view.handle_input("}", cx); + view.handle_input("}", cx); + }); + cx.assert_editor_state( + &" + 🏀{{{)}}}}ˇ + ε{{{)}}}}ˇ + ❤️{{{)}}}}ˇ + " + .unindent(), + ); + + // autoclose multi-character pairs + cx.set_state( + &" + ˇ + ˇ + " + .unindent(), + ); + cx.update_editor(|view, cx| { + view.handle_input("/", cx); + view.handle_input("*", cx); + }); + cx.assert_editor_state( + &" + /*ˇ */ + /*ˇ */ + " + .unindent(), + ); + + // one cursor autocloses a multi-character pair, one cursor + // does not autoclose. + cx.set_state( + &" + /ˇ + ˇ + " + .unindent(), + ); + cx.update_editor(|view, cx| view.handle_input("*", cx)); + cx.assert_editor_state( + &" + /*ˇ */ + *ˇ + " + .unindent(), + ); + + // Don't autoclose if the next character isn't whitespace and isn't + // listed in the language's "autoclose_before" section. + cx.set_state("ˇa b"); + cx.update_editor(|view, cx| view.handle_input("{", cx)); + cx.assert_editor_state("{ˇa b"); + + // Don't autoclose if `close` is false for the bracket pair + cx.set_state("ˇ"); + cx.update_editor(|view, cx| view.handle_input("[", cx)); + cx.assert_editor_state("[ˇ"); + + // Surround with brackets if text is selected + cx.set_state("«aˇ» b"); + cx.update_editor(|view, cx| view.handle_input("{", cx)); + cx.assert_editor_state("{«aˇ»} b"); + + // Autclose pair where the start and end characters are the same + cx.set_state("aˇ"); + cx.update_editor(|view, cx| view.handle_input("\"", cx)); + cx.assert_editor_state("a\"ˇ\""); + cx.update_editor(|view, cx| view.handle_input("\"", cx)); + cx.assert_editor_state("a\"\"ˇ"); +} + +#[gpui::test] +async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let html_language = Arc::new( + Language::new( + LanguageConfig { + name: "HTML".into(), + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "<".into(), + end: ">".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "{".into(), + end: "}".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "(".into(), + end: ")".into(), + close: true, + ..Default::default() + }, + ], + ..Default::default() + }, + autoclose_before: "})]>".into(), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_injection_query( + r#" + (script_element + (raw_text) @content + (#set! "language" "javascript")) + "#, + ) + .unwrap(), + ); + + let javascript_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "/*".into(), + end: " */".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "{".into(), + end: "}".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "(".into(), + end: ")".into(), + close: true, + ..Default::default() + }, + ], + ..Default::default() + }, + autoclose_before: "})]>".into(), + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(html_language.clone()); + registry.add(javascript_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(html_language), cx); + }); + + cx.set_state( + &r#" + ˇ + + ˇ + "# + .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()] + ); + }); + + // 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(), + ); +} + +#[gpui::test] +async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let rust_language = Arc::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + brackets: serde_json::from_value(json!([ + { "start": "{", "end": "}", "close": true, "newline": true }, + { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] }, + ])) + .unwrap(), + autoclose_before: "})]>".into(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_override_query("(string_literal) @string") + .unwrap(), + ); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(rust_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(rust_language), cx); + }); + + cx.set_state( + &r#" + let x = ˇ + "# + .unindent(), + ); + + // Inserting a quotation mark. A closing quotation mark is automatically inserted. + cx.update_editor(|editor, cx| { + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = "ˇ" + "# + .unindent(), + ); + + // Inserting another quotation mark. The cursor moves across the existing + // automatically-inserted quotation mark. + cx.update_editor(|editor, cx| { + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = ""ˇ + "# + .unindent(), + ); + + // Reset + cx.set_state( + &r#" + let x = ˇ + "# + .unindent(), + ); + + // Inserting a quotation mark inside of a string. A second quotation mark is not inserted. + cx.update_editor(|editor, cx| { + editor.handle_input("\"", cx); + editor.handle_input(" ", cx); + editor.move_left(&Default::default(), cx); + editor.handle_input("\\", cx); + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = "\"ˇ " + "# + .unindent(), + ); + + // Inserting a closing quotation mark at the position of an automatically-inserted quotation + // mark. Nothing is inserted. + cx.update_editor(|editor, cx| { + editor.move_right(&Default::default(), cx); + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = "\" "ˇ + "# + .unindent(), + ); +} + +#[gpui::test] +async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/* ".to_string(), + end: "*/".to_string(), + close: true, + ..Default::default() + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = r#" + a + b + c + "# + .unindent(); + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), + ]) + }); + + view.handle_input("{", cx); + view.handle_input("{", cx); + view.handle_input("{", cx); + assert_eq!( + view.text(cx), + " + {{{a}}} + {{{b}}} + {{{c}}} + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4) + ] + ); + + view.undo(&Undo, cx); + view.undo(&Undo, cx); + view.undo(&Undo, cx); + assert_eq!( + view.text(cx), + " + a + b + c + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) + ] + ); + + // Ensure inserting the first character of a multi-byte bracket pair + // doesn't surround the selections with the bracket. + view.handle_input("/", cx); + assert_eq!( + view.text(cx), + " + / + / + / + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) + ] + ); + + view.undo(&Undo, cx); + assert_eq!( + view.text(cx), + " + a + b + c + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) + ] + ); + + // Ensure inserting the last character of a multi-byte bracket pair + // doesn't surround the selections with the bracket. + view.handle_input("*", cx); + assert_eq!( + view.text(cx), + " + * + * + * + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) + ] + ); + }); +} + +#[gpui::test] +async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }], + ..Default::default() + }, + autoclose_before: "}".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = r#" + a + b + c + "# + .unindent(); + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor + .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + Point::new(2, 1)..Point::new(2, 1), + ]) + }); + + editor.handle_input("{", cx); + editor.handle_input("{", cx); + editor.handle_input("_", cx); + assert_eq!( + editor.text(cx), + " + a{{_}} + b{{_}} + c{{_}} + " + .unindent() + ); + assert_eq!( + editor.selections.ranges::(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) + ] + ); + }); +} + +#[gpui::test] +async fn test_snippets(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let (text, insertion_ranges) = marked_text_ranges( + indoc! {" + a.ˇ b + a.ˇ b + a.ˇ b + "}, + false, + ); + + let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + + editor.update(cx, |editor, cx| { + let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); + + editor + .insert_snippet(&insertion_ranges, snippet, cx) + .unwrap(); + + fn assert(editor: &mut Editor, cx: &mut ViewContext, 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); + } + + assert( + editor, + cx, + indoc! {" + a.f(«one», two, «three») b + a.f(«one», two, «three») b + a.f(«one», two, «three») b + "}, + ); + + // Can't move earlier than the first tab stop + assert!(!editor.move_to_prev_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(«one», two, «three») b + a.f(«one», two, «three») b + a.f(«one», two, «three») b + "}, + ); + + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(one, «two», three) b + a.f(one, «two», three) b + a.f(one, «two», three) b + "}, + ); + + editor.move_to_prev_snippet_tabstop(cx); + assert( + editor, + cx, + indoc! {" + a.f(«one», two, «three») b + a.f(«one», two, «three») b + a.f(«one», two, «three») b + "}, + ); + + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(one, «two», three) b + a.f(one, «two», three) b + a.f(one, «two», three) b + "}, + ); + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + "}, + ); + + // As soon as the last tab stop is reached, snippet state is gone + editor.move_to_prev_snippet_tabstop(cx); + assert( + editor, + cx, + indoc! {" + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + "}, + ); + }); +} + +#[gpui::test] +async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 4); + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )])) + }) + .next() + .await; + cx.foreground().start_waiting(); + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + // Ensure we can still save even if formatting hangs. + fake_server.handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }); + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + cx.foreground().advance_clock(super::FORMAT_TIMEOUT); + cx.foreground().start_waiting(); + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + // Set rust language override and assert overridden tabsize is sent to language server + update_test_language_settings(cx, |settings| { + settings.languages.insert( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); + }); + + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 8); + Ok(Some(vec![])) + }) + .next() + .await; + cx.foreground().start_waiting(); + save.await.unwrap(); +} + +#[gpui::test] +async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_range_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 4); + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )])) + }) + .next() + .await; + cx.foreground().start_waiting(); + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + // Ensure we can still save even if formatting hangs. + fake_server.handle_request::( + move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }, + ); + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + cx.foreground().advance_clock(super::FORMAT_TIMEOUT); + cx.foreground().start_waiting(); + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + // Set rust language override and assert overridden tabsize is sent to language server + update_test_language_settings(cx, |settings| { + settings.languages.insert( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); + }); + + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 8); + Ok(Some(vec![])) + }) + .next() + .await; + cx.foreground().start_waiting(); + save.await.unwrap(); +} + +#[gpui::test] +async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + // Enable Prettier formatting for the same buffer, and ensure + // LSP is called instead of Prettier. + prettier_parser_name: Some("test_parser".to_string()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 4); + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )])) + }) + .next() + .await; + cx.foreground().start_waiting(); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + // Ensure we don't lock if formatting hangs. + fake_server.handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }); + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project, FormatTrigger::Manual, cx) + }); + cx.foreground().advance_clock(super::FORMAT_TIMEOUT); + cx.foreground().start_waiting(); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); +} + +#[gpui::test] +async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + one.twoˇ + "}); + + // The format request takes a long time. When it completes, it inserts + // a newline and an indent before the `.` + cx.lsp + .handle_request::(move |_, cx| { + let executor = cx.background(); + async move { + executor.timer(Duration::from_millis(100)).await; + Ok(Some(vec![lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)), + new_text: "\n ".into(), + }])) + } + }); + + // Submit a format request. + let format_1 = cx + .update_editor(|editor, cx| editor.format(&Format, cx)) + .unwrap(); + cx.foreground().run_until_parked(); + + // Submit a second format request. + let format_2 = cx + .update_editor(|editor, cx| editor.format(&Format, cx)) + .unwrap(); + cx.foreground().run_until_parked(); + + // Wait for both format requests to complete + cx.foreground().advance_clock(Duration::from_millis(200)); + cx.foreground().start_waiting(); + format_1.await.unwrap(); + cx.foreground().start_waiting(); + format_2.await.unwrap(); + + // The formatting edits only happens once. + cx.assert_editor_state(indoc! {" + one + .twoˇ + "}); +} + +#[gpui::test] +async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Auto) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Set up a buffer white some trailing whitespace and no trailing newline. + cx.set_state( + &[ + "one ", // + "twoˇ", // + "three ", // + "four", // + ] + .join("\n"), + ); + + // Submit a format request. + let format = cx + .update_editor(|editor, cx| editor.format(&Format, cx)) + .unwrap(); + + // Record which buffer changes have been sent to the language server + let buffer_changes = Arc::new(Mutex::new(Vec::new())); + cx.lsp + .handle_notification::({ + let buffer_changes = buffer_changes.clone(); + move |params, _| { + buffer_changes.lock().extend( + params + .content_changes + .into_iter() + .map(|e| (e.range.unwrap(), e.text)), + ); + } + }); + + // Handle formatting requests to the language server. + cx.lsp.handle_request::({ + let buffer_changes = buffer_changes.clone(); + move |_, _| { + // When formatting is requested, trailing whitespace has already been stripped, + // and the trailing newline has already been added. + assert_eq!( + &buffer_changes.lock()[1..], + &[ + ( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)), + "".into() + ), + ( + lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), + "".into() + ), + ( + lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)), + "\n".into() + ), + ] + ); + + // Insert blank lines between each line of the buffer. + async move { + Ok(Some(vec![ + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), + new_text: "\n".into(), + }, + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)), + new_text: "\n".into(), + }, + ])) + } + } + }); + + // After formatting the buffer, the trailing whitespace is stripped, + // a newline is appended, and the edits provided by the language server + // have been applied. + format.await.unwrap(); + cx.assert_editor_state( + &[ + "one", // + "", // + "twoˇ", // + "", // + "three", // + "four", // + "", // + ] + .join("\n"), + ); + + // Undoing the formatting undoes the trailing whitespace removal, the + // trailing newline, and the LSP edits. + cx.update_buffer(|buffer, cx| buffer.undo(cx)); + cx.assert_editor_state( + &[ + "one ", // + "twoˇ", // + "three ", // + "four", // + ] + .join("\n"), + ); +} + +#[gpui::test] +async fn test_completion(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec!["first_completion", "second_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor.context_menu_next(&Default::default(), cx); + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {" + one.second_completionˇ + two + three + "}); + + handle_resolve_completion_request( + &mut cx, + Some(vec![ + ( + //This overlaps with the primary completion edit which is + //misbehavior from the LSP spec, test that we filter it out + indoc! {" + one.second_ˇcompletion + two + threeˇ + "}, + "overlapping additional edit", + ), + ( + indoc! {" + one.second_completion + two + threeˇ + "}, + "\nadditional edit", + ), + ]), + ) + .await; + apply_additional_edits.await.unwrap(); + cx.assert_editor_state(indoc! {" + one.second_completionˇ + two + three + additional edit + "}); + + cx.set_state(indoc! {" + one.second_completion + twoˇ + threeˇ + additional edit + "}); + cx.simulate_keystroke(" "); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.simulate_keystroke("s"); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + + cx.assert_editor_state(indoc! {" + one.second_completion + two sˇ + three sˇ + additional edit + "}); + handle_completion_request( + &mut cx, + indoc! {" + one.second_completion + two s + three + additional edit + "}, + vec!["fourth_completion", "fifth_completion", "sixth_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + + cx.simulate_keystroke("i"); + + handle_completion_request( + &mut cx, + indoc! {" + one.second_completion + two si + three + additional edit + "}, + vec!["fourth_completion", "fifth_completion", "sixth_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {" + one.second_completion + two sixth_completionˇ + three sixth_completionˇ + additional edit + "}); + + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); + + cx.update(|cx| { + cx.update_global::(|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_toggle_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // If multiple selections intersect a line, the line is only toggled once. + cx.set_state(indoc! {" + fn a() { + «//b(); + ˇ»// «c(); + //ˇ» d(); + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + «b(); + c(); + ˇ» d(); + } + "}); + + // The comment prefix is inserted at the same column for every line in a + // selection. + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // «b(); + // c(); + ˇ»// d(); + } + "}); + + // If a selection ends at the beginning of a line, that line is not toggled. + cx.set_selections_state(indoc! {" + fn a() { + // b(); + «// c(); + ˇ» // d(); + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // b(); + «c(); + ˇ» // d(); + } + "}); + + // If a selection span a single line and is empty, the line is toggled. + cx.set_state(indoc! {" + fn a() { + a(); + b(); + ˇ + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + a(); + b(); + //•ˇ + } + "}); + + // If a selection span multiple lines, empty lines are not toggled. + cx.set_state(indoc! {" + fn a() { + «a(); + + c();ˇ» + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // «a(); + + // c();ˇ» + } + "}); +} + +#[gpui::test] +async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(language.clone()); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(language), cx); + }); + + let toggle_comments = &ToggleComments { + advance_downwards: true, + }; + + // Single cursor on one line -> advance + // Cursor moves horizontally 3 characters as well on non-blank line + cx.set_state(indoc!( + "fn a() { + ˇdog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + catˇ(); + }" + )); + + // Single selection on one line -> don't advance + cx.set_state(indoc!( + "fn a() { + «dog()ˇ»; + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // «dog()ˇ»; + cat(); + }" + )); + + // Multiple cursors on one line -> advance + cx.set_state(indoc!( + "fn a() { + ˇdˇog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + catˇ(ˇ); + }" + )); + + // Multiple cursors on one line, with selection -> don't advance + cx.set_state(indoc!( + "fn a() { + ˇdˇog«()ˇ»; + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // ˇdˇog«()ˇ»; + cat(); + }" + )); + + // Single cursor on one line -> advance + // Cursor moves to column 0 on blank line + cx.set_state(indoc!( + "fn a() { + ˇdog(); + + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + ˇ + cat(); + }" + )); + + // Single cursor on one line -> advance + // Cursor starts and ends at column 0 + cx.set_state(indoc!( + "fn a() { + ˇ dog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + ˇ cat(); + }" + )); +} + +#[gpui::test] +async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let html_language = Arc::new( + Language::new( + LanguageConfig { + name: "HTML".into(), + block_comment: Some(("".into())), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_injection_query( + r#" + (script_element + (raw_text) @content + (#set! "language" "javascript")) + "#, + ) + .unwrap(), + ); + + let javascript_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(html_language.clone()); + registry.add(javascript_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(html_language), cx); + }); + + // Toggle comments for empty selections + cx.set_state( + &r#" +

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, |_| {}); + + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(0, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(1, 0)..Point::new(1, 4), + primary: None, + }, + ], + cx, + ); + assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb"); + multibuffer + }); + + let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + view.update(cx, |view, cx| { + assert_eq!(view.text(cx), "aaaa\nbbbb"); + view.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 0)..Point::new(0, 0), + Point::new(1, 0)..Point::new(1, 0), + ]) + }); + + view.handle_input("X", cx); + assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); + assert_eq!( + view.selections.ranges(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + ] + ); + + // Ensure the cursor's head is respected when deleting across an excerpt boundary. + view.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) + }); + view.backspace(&Default::default(), cx); + assert_eq!(view.text(cx), "Xa\nbbb"); + assert_eq!( + view.selections.ranges(cx), + [Point::new(1, 0)..Point::new(1, 0)] + ); + + view.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) + }); + view.backspace(&Default::default(), cx); + assert_eq!(view.text(cx), "X\nbb"); + assert_eq!( + view.selections.ranges(cx), + [Point::new(0, 1)..Point::new(0, 1)] + ); + }); +} + +#[gpui::test] +fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let markers = vec![('[', ']').into(), ('(', ')').into()]; + let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( + indoc! {" + [aaaa + (bbbb] + cccc)", + }, + markers.clone(), + ); + let excerpt_ranges = markers.into_iter().map(|marker| { + let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); + ExcerptRange { + context, + primary: None, + } + }); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text)); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts(buffer, excerpt_ranges, cx); + multibuffer + }); + + let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + view.update(cx, |view, cx| { + let (expected_text, selection_ranges) = marked_text_ranges( + indoc! {" + aaaa + bˇbbb + bˇbbˇb + cccc" + }, + true, + ); + assert_eq!(view.text(cx), expected_text); + view.change_selections(None, cx, |s| s.select_ranges(selection_ranges)); + + view.handle_input("X", cx); + + let (expected_text, expected_selections) = marked_text_ranges( + indoc! {" + aaaa + bXˇbbXb + bXˇbbXˇb + cccc" + }, + false, + ); + assert_eq!(view.text(cx), expected_text); + assert_eq!(view.selections.ranges(cx), expected_selections); + + view.newline(&Newline, cx); + let (expected_text, expected_selections) = marked_text_ranges( + indoc! {" + aaaa + bX + ˇbbX + b + bX + ˇbbX + ˇb + cccc" + }, + false, + ); + assert_eq!(view.text(cx), expected_text); + assert_eq!(view.selections.ranges(cx), expected_selections); + }); +} + +#[gpui::test] +fn test_refresh_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); + let mut excerpt1_id = None; + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + excerpt1_id = multibuffer + .push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 4), + primary: None, + }, + ], + cx, + ) + .into_iter() + .next(); + assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); + multibuffer + }); + + let editor = cx + .add_window(|cx| { + let mut editor = build_editor(multibuffer.clone(), cx); + let snapshot = editor.snapshot(cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) + }); + editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ] + ); + editor + }) + .root(cx); + + // Refreshing selections is a no-op when excerpts haven't changed. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.refresh()); + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ] + ); + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); + }); + editor.update(cx, |editor, cx| { + // Removing an excerpt causes the first selection to become degenerate. + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(0, 0)..Point::new(0, 0), + Point::new(0, 1)..Point::new(0, 1) + ] + ); + + // Refreshing selections will relocate the first selection to the original buffer + // location. + editor.change_selections(None, cx, |s| s.refresh()); + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(0, 3)..Point::new(0, 3) + ] + ); + assert!(editor.selections.pending_anchor().is_some()); + }); +} + +#[gpui::test] +fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); + let mut excerpt1_id = None; + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + excerpt1_id = multibuffer + .push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 4), + primary: None, + }, + ], + cx, + ) + .into_iter() + .next(); + assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); + multibuffer + }); + + let editor = cx + .add_window(|cx| { + let mut editor = build_editor(multibuffer.clone(), cx); + let snapshot = editor.snapshot(cx); + editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); + assert_eq!( + editor.selections.ranges(cx), + [Point::new(1, 3)..Point::new(1, 3)] + ); + editor + }) + .root(cx); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); + }); + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.ranges(cx), + [Point::new(0, 0)..Point::new(0, 0)] + ); + + // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. + editor.change_selections(None, cx, |s| s.refresh()); + assert_eq!( + editor.selections.ranges(cx), + [Point::new(0, 3)..Point::new(0, 3)] + ); + assert!(editor.selections.pending_anchor().is_some()); + }); +} + +#[gpui::test] +async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/* ".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query("") + .unwrap(), + ); + + let text = concat!( + "{ }\n", // + " x\n", // + " /* */\n", // + "x\n", // + "{{} }\n", // + ); + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ]) + }); + view.newline(&Newline, cx); + + assert_eq!( + view.buffer().read(cx).read(cx).text(), + concat!( + "{ \n", // Suppress rustfmt + "\n", // + "}\n", // + " x\n", // + " /* \n", // + " \n", // + " */\n", // + "x\n", // + "{{} \n", // + "}\n", // + ) + ); + }); +} + +#[gpui::test] +fn test_highlighted_ranges(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + build_editor(buffer.clone(), cx) + }) + .root(cx); + + editor.update(cx, |editor, cx| { + struct Type1; + struct Type2; + + let buffer = editor.buffer.read(cx).snapshot(cx); + + let anchor_range = + |range: Range| buffer.anchor_after(range.start)..buffer.anchor_after(range.end); + + editor.highlight_background::( + vec![ + anchor_range(Point::new(2, 1)..Point::new(2, 3)), + anchor_range(Point::new(4, 2)..Point::new(4, 4)), + anchor_range(Point::new(6, 3)..Point::new(6, 5)), + anchor_range(Point::new(8, 4)..Point::new(8, 6)), + ], + |_| Hsla::red(), + cx, + ); + editor.highlight_background::( + vec![ + anchor_range(Point::new(3, 2)..Point::new(3, 5)), + anchor_range(Point::new(5, 3)..Point::new(5, 6)), + anchor_range(Point::new(7, 4)..Point::new(7, 7)), + anchor_range(Point::new(9, 5)..Point::new(9, 8)), + ], + |_| Hsla::green(), + cx, + ); + + let snapshot = editor.snapshot(cx); + let mut highlighted_ranges = editor.background_highlights_in_range( + anchor_range(Point::new(3, 4)..Point::new(7, 4)), + &snapshot, + theme::current(cx).as_ref(), + ); + // Enforce a consistent ordering based on color without relying on the ordering of the + // highlight's `TypeId` which is non-deterministic. + highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); + assert_eq!( + highlighted_ranges, + &[ + ( + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), + Hsla::green(), + ), + ( + DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), + Hsla::green(), + ), + ( + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), + Hsla::red(), + ), + ( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Hsla::red(), + ), + ] + ); + assert_eq!( + editor.background_highlights_in_range( + anchor_range(Point::new(5, 6)..Point::new(6, 4)), + &snapshot, + theme::current(cx).as_ref(), + ), + &[( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Hsla::red(), + )] + ); + }); +} + +#[gpui::test] +async fn test_following(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + + let buffer = project.update(cx, |project, cx| { + let buffer = project + .create_buffer(&sample_text(16, 8, 'a'), None, cx) + .unwrap(); + cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) + }); + let leader = cx + .add_window(|cx| build_editor(buffer.clone(), cx)) + .root(cx); + let follower = cx + .update(|cx| { + cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), + ..Default::default() + }, + |cx| build_editor(buffer.clone(), cx), + ) + }) + .root(cx); + + let is_still_following = Rc::new(RefCell::new(true)); + let follower_edit_event_count = Rc::new(RefCell::new(0)); + let pending_update = Rc::new(RefCell::new(None)); + follower.update(cx, { + let update = pending_update.clone(); + let is_still_following = is_still_following.clone(); + let follower_edit_event_count = follower_edit_event_count.clone(); + |_, cx| { + cx.subscribe(&leader, move |_, leader, event, cx| { + leader + .read(cx) + .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + }) + .detach(); + + cx.subscribe(&follower, move |_, _, event, cx| { + if Editor::should_unfollow_on_event(event, cx) { + *is_still_following.borrow_mut() = false; + } + if let Event::BufferEdited = event { + *follower_edit_event_count.borrow_mut() += 1; + } + }) + .detach(); + } + }); + + // Update the selections only + leader.update(cx, |leader, cx| { + leader.change_selections(None, cx, |s| s.select_ranges([1..1])); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![1..1]); + }); + assert_eq!(*is_still_following.borrow(), true); + assert_eq!(*follower_edit_event_count.borrow(), 0); + + // Update the scroll position only + leader.update(cx, |leader, cx| { + leader.set_scroll_position(vec2f(1.5, 3.5), cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + assert_eq!( + follower.update(cx, |follower, cx| follower.scroll_position(cx)), + vec2f(1.5, 3.5) + ); + assert_eq!(*is_still_following.borrow(), true); + assert_eq!(*follower_edit_event_count.borrow(), 0); + + // Update the selections and scroll position. The follower's scroll position is updated + // via autoscroll, not via the leader's exact scroll position. + leader.update(cx, |leader, cx| { + leader.change_selections(None, cx, |s| s.select_ranges([0..0])); + leader.request_autoscroll(Autoscroll::newest(), cx); + leader.set_scroll_position(vec2f(1.5, 3.5), cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.update(cx, |follower, cx| { + assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0)); + assert_eq!(follower.selections.ranges(cx), vec![0..0]); + }); + assert_eq!(*is_still_following.borrow(), true); + + // Creating a pending selection that precedes another selection + leader.update(cx, |leader, cx| { + leader.change_selections(None, cx, |s| s.select_ranges([1..1])); + leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); + }); + assert_eq!(*is_still_following.borrow(), true); + + // Extend the pending selection so that it surrounds another selection + leader.update(cx, |leader, cx| { + leader.extend_selection(DisplayPoint::new(0, 2), 1, cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..2]); + }); + + // Scrolling locally breaks the follow + follower.update(cx, |follower, cx| { + let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0); + follower.set_scroll_anchor( + ScrollAnchor { + anchor: top_anchor, + offset: vec2f(0.0, 0.5), + }, + cx, + ); + }); + assert_eq!(*is_still_following.borrow(), false); +} + +#[gpui::test] +async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + let leader = pane.update(cx, |_, cx| { + let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + cx.add_view(|cx| build_editor(multibuffer.clone(), cx)) + }); + + // Start following the editor when it has no excerpts. + let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); + let follower_1 = cx + .update(|cx| { + Editor::from_state_proto( + pane.clone(), + workspace.clone(), + ViewId { + creator: Default::default(), + id: 0, + }, + &mut state_message, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + + let update_message = Rc::new(RefCell::new(None)); + follower_1.update(cx, { + let update = update_message.clone(); + |_, cx| { + cx.subscribe(&leader, move |_, leader, event, cx| { + leader + .read(cx) + .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + }) + .detach(); + } + }); + + let (buffer_1, buffer_2) = project.update(cx, |project, cx| { + ( + project + .create_buffer("abc\ndef\nghi\njkl\n", None, cx) + .unwrap(), + project + .create_buffer("mno\npqr\nstu\nvwx\n", None, cx) + .unwrap(), + ) + }); + + // Insert some excerpts. + leader.update(cx, |leader, cx| { + leader.buffer.update(cx, |multibuffer, cx| { + let excerpt_ids = multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: 1..6, + primary: None, + }, + ExcerptRange { + context: 12..15, + primary: None, + }, + ExcerptRange { + context: 0..3, + primary: None, + }, + ], + cx, + ); + multibuffer.insert_excerpts_after( + excerpt_ids[0], + buffer_2.clone(), + [ + ExcerptRange { + context: 8..12, + primary: None, + }, + ExcerptRange { + context: 0..6, + primary: None, + }, + ], + cx, + ); + }); + }); + + // Apply the update of adding the excerpts. + follower_1 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + assert_eq!( + follower_1.read_with(cx, |editor, cx| editor.text(cx)), + leader.read_with(cx, |editor, cx| editor.text(cx)) + ); + update_message.borrow_mut().take(); + + // Start following separately after it already has excerpts. + let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); + let follower_2 = cx + .update(|cx| { + Editor::from_state_proto( + pane.clone(), + workspace.clone(), + ViewId { + creator: Default::default(), + id: 0, + }, + &mut state_message, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + assert_eq!( + follower_2.read_with(cx, |editor, cx| editor.text(cx)), + leader.read_with(cx, |editor, cx| editor.text(cx)) + ); + + // Remove some excerpts. + leader.update(cx, |leader, cx| { + leader.buffer.update(cx, |multibuffer, cx| { + let excerpt_ids = multibuffer.excerpt_ids(); + multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx); + multibuffer.remove_excerpts([excerpt_ids[0]], cx); + }); + }); + + // Apply the update of removing the excerpts. + follower_1 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + follower_2 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + update_message.borrow_mut().take(); + assert_eq!( + follower_1.read_with(cx, |editor, cx| editor.text(cx)), + leader.read_with(cx, |editor, cx| editor.text(cx)) + ); +} + +#[test] +fn test_combine_syntax_and_fuzzy_match_highlights() { + let string = "abcdefghijklmnop"; + let syntax_ranges = [ + ( + 0..3, + HighlightStyle { + color: Some(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() + }, + ), + ] + ); +} + +#[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() + }); + }); + + deterministic.run_until_parked(); + + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); + + cx.assert_editor_state(indoc! {" + fn func(abc def: i32) -> ˇu32 { + } + "}); + + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); + + cx.assert_editor_state(indoc! {" + fn func(abc ˇdef: i32) -> u32 { + } + "}); + + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); + + cx.assert_editor_state(indoc! {" + fn func(abcˇ def: i32) -> u32 { + } + "}); + + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); + + cx.assert_editor_state(indoc! {" + fn func(abc def: i32) -> ˇu32 { + } + "}); +} + +#[gpui::test] +async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let diff_base = r#" + use some::mod; + + const A: u32 = 42; + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(); + + // Edits are modified, removed, modified, added + cx.set_state( + &r#" + use some::modified; + + ˇ + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.set_diff_base(Some(&diff_base)); + deterministic.run_until_parked(); + + cx.update_editor(|editor, cx| { + //Wrap around the bottom of the buffer + for _ in 0..3 { + editor.go_to_hunk(&GoToHunk, cx); + } + }); + + cx.assert_editor_state( + &r#" + ˇuse some::modified; + + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + //Wrap around the top of the buffer + for _ in 0..2 { + editor.go_to_prev_hunk(&GoToPrevHunk, cx); + } + }); + + cx.assert_editor_state( + &r#" + use some::modified; + + fn main() { + ˇ println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + editor.go_to_prev_hunk(&GoToPrevHunk, cx); + }); + + cx.assert_editor_state( + &r#" + use some::modified; + + ˇ + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + for _ in 0..3 { + editor.go_to_prev_hunk(&GoToPrevHunk, cx); + } + }); + + cx.assert_editor_state( + &r#" + use some::modified; + + fn main() { + ˇ println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + editor.fold(&Fold, cx); + + //Make sure that the fold only gets one hunk + for _ in 0..4 { + editor.go_to_hunk(&GoToHunk, cx); + } + }); + + cx.assert_editor_state( + &r#" + ˇuse some::modified; + + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); +} + +#[test] +fn test_split_words() { + fn split<'a>(text: &'a str) -> Vec<&'a str> { + split_words(text).collect() + } + + assert_eq!(split("HelloWorld"), &["Hello", "World"]); + assert_eq!(split("hello_world"), &["hello_", "world"]); + assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]); + assert_eq!(split("Hello_World"), &["Hello_", "World"]); + assert_eq!(split("helloWOrld"), &["hello", "WOrld"]); + assert_eq!(split("helloworld"), &["helloworld"]); +} + +#[gpui::test] +async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; + let mut assert = |before, after| { + let _state_context = cx.set_state(before); + cx.update_editor(|editor, cx| { + editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx) + }); + cx.assert_editor_state(after); + }; + + // Outside bracket jumps to outside of matching bracket + assert("console.logˇ(var);", "console.log(var)ˇ;"); + assert("console.log(var)ˇ;", "console.logˇ(var);"); + + // Inside bracket jumps to inside of matching bracket + assert("console.log(ˇvar);", "console.log(varˇ);"); + assert("console.log(varˇ);", "console.log(ˇvar);"); + + // When outside a bracket and inside, favor jumping to the inside bracket + assert( + "console.log('foo', [1, 2, 3]ˇ);", + "console.log(ˇ'foo', [1, 2, 3]);", + ); + assert( + "console.log(ˇ'foo', [1, 2, 3]);", + "console.log('foo', [1, 2, 3]ˇ);", + ); + + // Bias forward if two options are equally likely + assert( + "let result = curried_fun()ˇ();", + "let result = curried_fun()()ˇ;", + ); + + // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller + assert( + indoc! {" + function test() { + console.log('test')ˇ + }"}, + indoc! {" + function test() { + console.logˇ('test') + }"}, + ); +} + +#[gpui::test(iterations = 10)] +async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + // When inserting, ensure autocompletion is favored over Copilot suggestions. + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec!["completion_a", "completion_b"], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot1".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.context_menu_visible()); + assert!(!editor.has_active_copilot_suggestion(cx)); + + // Confirming a completion inserts it and hides the context menu, without showing + // the copilot suggestion afterwards. + editor + .confirm_completion(&Default::default(), cx) + .unwrap() + .detach(); + assert!(!editor.context_menu_visible()); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); + assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); + }); + + // Ensure Copilot suggestions are shown right away if no autocompletion is available. + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec![], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot1".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); + }); + + // Reset editor, and ensure autocompletion is still favored over Copilot suggestions. + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec!["completion_a", "completion_b"], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot1".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.context_menu_visible()); + assert!(!editor.has_active_copilot_suggestion(cx)); + + // When hiding the context menu, the Copilot suggestion becomes visible. + editor.hide_context_menu(cx); + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); + }); + + // Ensure existing completion is interpolated when inserting again. + cx.simulate_keystroke("c"); + deterministic.run_until_parked(); + cx.update_editor(|editor, cx| { + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + }); + + // After debouncing, new Copilot completions should be requested. + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot2".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), + ..Default::default() + }], + vec![], + ); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + + // Canceling should remove the active Copilot suggestion. + editor.cancel(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + + // After canceling, tabbing shouldn't insert the previously shown suggestion. + editor.tab(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); + + // When undoing the previously active suggestion is shown again. + editor.undo(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + }); + + // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. + cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); + + // Tabbing when there is an active suggestion inserts it. + editor.tab(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); + + // When undoing the previously active suggestion is shown again. + editor.undo(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); + + // Hide suggestion. + editor.cancel(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); + }); + + // If an edit occurs outside of this editor but no suggestion is being shown, + // we won't make it visible. + cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); + cx.update_editor(|editor, cx| { + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); + }); + + // Reset the editor to verify how suggestions behave when tabbing on leading indentation. + cx.update_editor(|editor, cx| { + editor.set_text("fn foo() {\n \n}", cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 2)..Point::new(1, 2)]) + }); + }); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: " let x = 4;".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), + ..Default::default() + }], + vec![], + ); + + cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); + assert_eq!(editor.text(cx), "fn foo() {\n \n}"); + + // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. + editor.tab(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "fn foo() {\n \n}"); + assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); + + // Tabbing again accepts the suggestion. + editor.tab(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); + assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); + }); +} + +#[gpui::test] +async fn test_copilot_completion_invalidation( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |_| {}); + + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + one + twˇ + three + "}); + + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "two.foo()".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), + ..Default::default() + }], + vec![], + ); + cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\ntw\nthree\n"); + + editor.backspace(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\nt\nthree\n"); + + editor.backspace(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\n\nthree\n"); + + // Deleting across the original suggestion range invalidates it. + editor.backspace(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\nthree\n"); + assert_eq!(editor.text(cx), "one\nthree\n"); + + // Undoing the deletion restores the suggestion. + editor.undo(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\n\nthree\n"); + }); +} + +#[gpui::test] +async fn test_copilot_multibuffer( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |_| {}); + + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + multibuffer + }); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "b = 2 + a".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)), + ..Default::default() + }], + vec![], + ); + editor.update(cx, |editor, cx| { + // Ensure copilot suggestions are shown for the first excerpt. + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 5)..Point::new(1, 5)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + editor.update(cx, |editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); + }); + + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "d = 4 + c".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)), + ..Default::default() + }], + vec![], + ); + editor.update(cx, |editor, cx| { + // Move to another excerpt, ensuring the suggestion gets cleared. + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) + }); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); + + // Type a character, ensuring we don't even try to interpolate the previous suggestion. + editor.handle_input(" ", cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); + }); + + // Ensure the new suggestion is displayed when the debounce timeout expires. + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + editor.update(cx, |editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); + }); +} + +#[gpui::test] +async fn test_copilot_disabled_globs( + deterministic: Arc, + 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); + }); + + cx.foreground().run_until_parked(); + + buffer.read_with(cx, |buffer, _| { + assert_eq!( + buffer.text(), + "fn main() { let a = {5}; }", + "No extra braces from on type formatting should appear in the buffer" + ) + }); +} + +#[gpui::test] +async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language_name: Arc = "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.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { let a = 5; }", + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let _fake_server = fake_servers.next().await.unwrap(); + update_test_language_settings(cx, |language_settings| { + language_settings.languages.insert( + Arc::clone(&language_name), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 0, + "Should not restart LSP server on an unrelated change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + "Some other server name".into(), + LspSettings { + initialization_options: Some(json!({ + "some other init value": false + })), + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 0, + "Should not restart LSP server on an unrelated LSP settings change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: Some(json!({ + "anotherInitValue": false + })), + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 1, + "Should restart LSP server on a related LSP settings change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: Some(json!({ + "anotherInitValue": false + })), + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 1, + "Should not restart LSP server on a related LSP settings change that is the same" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: None, + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 2, + "Should restart LSP server on another related LSP settings change" + ); +} + +#[gpui::test] +async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); + cx.simulate_keystroke("."); + let completion_item = lsp::CompletionItem { + label: "some".into(), + kind: Some(lsp::CompletionItemKind::SNIPPET), + detail: Some("Wrap the expression in an `Option::Some`".to_string()), + documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "```rust\nSome(2)\n```".to_string(), + })), + deprecated: Some(false), + sort_text: Some("fffffff2".to_string()), + filter_text: Some("some".to_string()), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 22, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "Some(2)".to_string(), + })), + additional_text_edits: Some(vec![lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 20, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "".to_string(), + }]), + ..Default::default() + }; + + let closure_completion_item = completion_item.clone(); + let mut request = cx.handle_request::(move |_, _, _| { + let task_completion_item = closure_completion_item.clone(); + async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + task_completion_item, + ]))) + } + }); + + request.next().await; + + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"}); + + cx.handle_request::(move |_, _, _| { + let task_completion_item = completion_item.clone(); + async move { Ok(task_completion_item) } + }) + .next() + .await + .unwrap(); + apply_additional_edits.await.unwrap(); + cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); +} + +#[gpui::test] +async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new( + Language::new( + LanguageConfig { + path_suffixes: vec!["jsx".into()], + overrides: [( + "element".into(), + LanguageConfigOverride { + word_characters: Override::Set(['-'].into_iter().collect()), + ..Default::default() + }, + )] + .into_iter() + .collect(), + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + ) + .with_override_query("(jsx_self_closing_element) @element") + .unwrap(), + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.lsp + .handle_request::(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "bg-blue".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-red".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-yellow".into(), + ..Default::default() + }, + ]))) + }); + + cx.set_state(r#"

"#); + + // Trigger completion when typing a dash, because the dash is an extra + // word character in the 'element' scope, which contains the cursor. + cx.simulate_keystroke("-"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-red", "bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); + + cx.simulate_keystroke("l"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); + + // When filtering completions, consider the character after the '-' to + // be the start of a subword. + cx.set_state(r#"

"#); + cx.simulate_keystroke("l"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); +} + +#[gpui::test] +async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Prettier) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + prettier_parser_name: Some("test_parser".to_string()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + + let test_plugin = "test_plugin"; + let _ = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + prettier_plugins: vec![test_plugin], + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + let buffer_text = "one\ntwo\nthree\n"; + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); + + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + buffer_text.to_string() + prettier_format_suffix, + "Test prettier formatting was not applied to the original buffer text", + ); + + update_test_language_settings(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Auto) + }); + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, + "Autoformatting (via test prettier) was not applied to the original buffer text", + ); +} + +fn empty_range(row: usize, column: usize) -> Range { + let point = DisplayPoint::new(row as u32, column as u32); + point..point +} + +fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext) { + let (text, ranges) = marked_text_ranges(marked_text, true); + assert_eq!(view.text(cx), text); + assert_eq!( + view.selections.ranges(cx), + ranges, + "Assert selections are {}", + marked_text + ); +} + +/// Handle completion request passing a marked string specifying where the completion +/// should be triggered from using '|' character, what range should be replaced, and what completions +/// should be returned using '<' and '>' to delimit the range +pub fn handle_completion_request<'a>( + cx: &mut EditorLspTestContext<'a>, + marked_string: &str, + completions: Vec<&'static str>, +) -> impl Future { + 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; + } +} + +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() + }) + } + }); + + 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(), + }) + } + }); +} + +pub(crate) fn update_test_language_settings( + cx: &mut TestAppContext, + f: impl Fn(&mut AllLanguageSettingsContent), +) { + cx.update(|cx| { + cx.update_global::(|store, 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(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + crate::init(cx); + }); + + update_test_language_settings(cx, f); +} diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index 14619c4a98..631e9409d4 100644 --- a/crates/editor2/src/test.rs +++ b/crates/editor2/src/test.rs @@ -1,81 +1,72 @@ 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 { + 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 { + Editor::new(EditorMode::Full, buffer, Some(project), None, cx) +} diff --git a/crates/editor2/src/test/editor_test_context.rs b/crates/editor2/src/test/editor_test_context.rs index 4bf32d0613..0ee6b33a2f 100644 --- a/crates/editor2/src/test/editor_test_context.rs +++ b/crates/editor2/src/test/editor_test_context.rs @@ -19,313 +19,313 @@ use util::{ // use super::build_editor_with_project; -// pub struct EditorTestContext<'a> { -// pub cx: &'a mut gpui::TestAppContext, -// pub window: AnyWindowHandle, -// pub editor: View, -// } +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, -// } -// } +impl<'a> EditorTestContext<'a> { + pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { + let fs = FakeFs::new(cx.executor().clone()); + // 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 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 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 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 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 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_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 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 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 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(); + 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); + self.cx.dispatch_keystroke(self.window, keystroke, false); -// keystroke_under_test_handle -// } + 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!(); -// } + 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 -// } + 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 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) -// } + 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) -// } + // 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)); -// } + 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 -// } + /// 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 -// } + /// 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(); + /// 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}"); -// } + 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()) -// } + 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) -// } + 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_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_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] + 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::>() -// } + 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! {" + #[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. + {}Editor has unexpected selections. -// Expected selections: -// {} + Expected selections: + {} -// Actual selections: -// {} -// "}, -// self.assertion_context(), -// expected_marked_text, -// actual_marked_text, -// ); -// } -// } -// } -// -// impl<'a> Deref for EditorTestContext<'a> { -// type Target = gpui::TestAppContext; + Actual selections: + {} + "}, + self.assertion_context(), + expected_marked_text, + actual_marked_text, + ); + } + } +} -// fn deref(&self) -> &Self::Target { -// self.cx -// } -// } +impl<'a> Deref for EditorTestContext<'a> { + type Target = gpui::TestAppContext; -// impl<'a> DerefMut for EditorTestContext<'a> { -// fn deref_mut(&mut self) -> &mut Self::Target { -// &mut self.cx -// } -// } + fn deref(&self) -> &Self::Target { + self.cx + } +} + +impl<'a> DerefMut for EditorTestContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx + } +}