From 0bfd18ba0992db8b960dac55298ffa7ba6923f7d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 8 Nov 2023 21:32:59 -0800 Subject: [PATCH 001/126] WIP --- crates/collab2/src/tests.rs | 1 + crates/collab2/src/tests/editor_tests.rs | 2218 +-- crates/editor2/src/editor_tests.rs | 16382 ++++++++-------- crates/editor2/src/test.rs | 125 +- .../editor2/src/test/editor_test_context.rs | 550 +- 5 files changed, 9635 insertions(+), 9641 deletions(-) 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 + } +} From 7a7ef1025ddde8d1a2aa8dbc30e49af01b8e1e4b Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 8 Nov 2023 22:11:19 -0800 Subject: [PATCH 002/126] Add Context::read_window, WindowHandle::root, and change ViewContext.view() to return a reference --- crates/gpui2/src/app.rs | 16 ++++++++++ crates/gpui2/src/app/async_context.rs | 18 +++++++++++ crates/gpui2/src/app/model_context.rs | 8 +++++ crates/gpui2/src/app/test_context.rs | 14 +++++++++ crates/gpui2/src/gpui2.rs | 6 ++++ crates/gpui2/src/input.rs | 2 +- crates/gpui2/src/window.rs | 45 ++++++++++++++++++++++++--- 7 files changed, 103 insertions(+), 6 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 356cf1b76b..f196923ad8 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1007,6 +1007,22 @@ impl Context for AppContext { let entity = self.entities.read(handle); read(entity, self) } + + fn read_window( + &self, + window: &AnyWindowHandle, + read: impl FnOnce(AnyView, &AppContext) -> R, + ) -> Result { + let window = self + .windows + .get(window.id) + .ok_or_else(|| anyhow!("window not found"))? + .as_ref() + .unwrap(); + + let root_view = window.root_view.clone().unwrap(); + Ok(read(root_view, self)) + } } /// These effects are processed at the end of each application update cycle. diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index e191e7315f..b9eeacb262 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -66,6 +66,16 @@ impl Context for AsyncAppContext { let mut lock = app.borrow_mut(); lock.update_window(window, f) } + + fn read_window( + &self, + window: &AnyWindowHandle, + read: impl FnOnce(AnyView, &AppContext) -> R, + ) -> Result { + let app = self.app.upgrade().context("app was released")?; + let lock = app.borrow(); + lock.read_window(window, read) + } } impl AsyncAppContext { @@ -250,6 +260,14 @@ impl Context for AsyncWindowContext { { self.app.read_model(handle, read) } + + fn read_window( + &self, + window: &AnyWindowHandle, + read: impl FnOnce(AnyView, &AppContext) -> R, + ) -> Result { + self.app.read_window(window, read) + } } impl VisualContext for AsyncWindowContext { diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index 44a3337f03..1fe9a09ba7 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -239,6 +239,14 @@ impl<'a, T> Context for ModelContext<'a, T> { { self.app.read_model(handle, read) } + + fn read_window( + &self, + window: &AnyWindowHandle, + read: impl FnOnce(AnyView, &AppContext) -> R, + ) -> Result { + self.app.read_window(window, read) + } } impl Borrow for ModelContext<'_, T> { diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 7b5ab5f7d7..f8d5c01160 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -58,6 +58,15 @@ impl Context for TestAppContext { let app = self.app.borrow(); app.read_model(handle, read) } + + fn read_window( + &self, + window: &AnyWindowHandle, + read: impl FnOnce(AnyView, &AppContext) -> R, + ) -> Result { + let app = self.app.borrow(); + app.read_window(window, read) + } } impl TestAppContext { @@ -146,6 +155,11 @@ impl TestAppContext { Some(read(lock.try_global()?, &lock)) } + pub fn set_global(&mut self, global: G) { + let mut lock = self.app.borrow_mut(); + lock.set_global(global); + } + pub fn update_global( &mut self, update: impl FnOnce(&mut G, &mut AppContext) -> R, diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 79275005d2..ff150824b1 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -104,6 +104,12 @@ pub trait Context { fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result where F: FnOnce(AnyView, &mut WindowContext<'_>) -> T; + + fn read_window( + &self, + window: &AnyWindowHandle, + read: impl FnOnce(AnyView, &AppContext) -> R, + ) -> Result; } pub trait VisualContext: Context { diff --git a/crates/gpui2/src/input.rs b/crates/gpui2/src/input.rs index d768ce946a..140f724417 100644 --- a/crates/gpui2/src/input.rs +++ b/crates/gpui2/src/input.rs @@ -45,7 +45,7 @@ impl ElementInputHandler { /// containing view. pub fn new(element_bounds: Bounds, cx: &mut ViewContext) -> Self { ElementInputHandler { - view: cx.view(), + view: cx.view().clone(), element_bounds, cx: cx.to_async(), } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index ac7dcf0256..a9d2d55453 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1428,6 +1428,19 @@ impl Context for WindowContext<'_> { let entity = self.entities.read(handle); read(&*entity, &*self.app) } + + fn read_window( + &self, + window: &AnyWindowHandle, + read: impl FnOnce(AnyView, &AppContext) -> R, + ) -> Result { + if window == &self.window.handle { + let root_view = self.window.root_view.clone().unwrap(); + Ok(read(root_view, self)) + } else { + window.read(self.app, read) + } + } } impl VisualContext for WindowContext<'_> { @@ -1747,9 +1760,8 @@ impl<'a, V: 'static> ViewContext<'a, V> { } } - // todo!("change this to return a reference"); - pub fn view(&self) -> View { - self.view.clone() + pub fn view(&self) -> &View { + self.view } pub fn model(&self) -> Model { @@ -1772,7 +1784,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { where V: 'static, { - let view = self.view(); + let view = self.view().clone(); self.window_cx.on_next_frame(move |cx| view.update(cx, f)); } @@ -2170,7 +2182,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { &mut self, handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, ) { - let handle = self.view(); + let handle = self.view().clone(); self.window_cx.on_mouse_event(move |event, phase, cx| { handle.update(cx, |view, cx| { handler(view, event, phase, cx); @@ -2244,6 +2256,14 @@ impl Context for ViewContext<'_, V> { { self.window_cx.read_model(handle, read) } + + fn read_window( + &self, + window: &AnyWindowHandle, + read: impl FnOnce(AnyView, &AppContext) -> R, + ) -> Result { + self.window_cx.read_window(window, read) + } } impl VisualContext for ViewContext<'_, V> { @@ -2315,6 +2335,14 @@ impl WindowHandle { } } + pub fn root(&self, cx: &C) -> Result> { + cx.read_window(&self.any_handle, |root_view, _| { + root_view + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed")) + })? + } + pub fn update( self, cx: &mut C, @@ -2395,6 +2423,13 @@ impl AnyWindowHandle { { cx.update_window(self, update) } + + pub fn read(self, cx: &C, read: impl FnOnce(AnyView, &AppContext) -> R) -> Result + where + C: Context, + { + cx.read_window(&self, read) + } } #[cfg(any(test, feature = "test-support"))] From 269c3ea2445dc716a2e83a6add76fc920595a315 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 8 Nov 2023 22:09:50 -0800 Subject: [PATCH 003/126] Uncomment tests --- .../collab2/src/tests/channel_buffer_tests.rs | 43 +- crates/collab2/src/tests/editor_tests.rs | 758 ++++++++- crates/collab2/src/tests/integration_tests.rs | 1385 ++++++++--------- crates/editor2/src/editor_tests.rs | 49 +- crates/editor2/src/test.rs | 4 +- .../src/test/editor_lsp_test_context.rs | 526 +++---- crates/zed2/src/main.rs | 3 +- 7 files changed, 1751 insertions(+), 1017 deletions(-) diff --git a/crates/collab2/src/tests/channel_buffer_tests.rs b/crates/collab2/src/tests/channel_buffer_tests.rs index ba891e6192..0da5256682 100644 --- a/crates/collab2/src/tests/channel_buffer_tests.rs +++ b/crates/collab2/src/tests/channel_buffer_tests.rs @@ -282,28 +282,27 @@ async fn test_core_channel_buffers( // }); // } -//todo!(editor) -// #[track_caller] -// fn assert_remote_selections( -// editor: &mut Editor, -// expected_selections: &[(Option, Range)], -// cx: &mut ViewContext, -// ) { -// let snapshot = editor.snapshot(cx); -// let range = Anchor::min()..Anchor::max(); -// let remote_selections = snapshot -// .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) -// .map(|s| { -// let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); -// let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); -// (s.participant_index, start..end) -// }) -// .collect::>(); -// assert_eq!( -// remote_selections, expected_selections, -// "incorrect remote selections" -// ); -// } +#[track_caller] +fn assert_remote_selections( + editor: &mut Editor, + expected_selections: &[(Option, Range)], + cx: &mut ViewContext, +) { + let snapshot = editor.snapshot(cx); + let range = Anchor::min()..Anchor::max(); + let remote_selections = snapshot + .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) + .map(|s| { + let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); + let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); + (s.participant_index, start..end) + }) + .collect::>(); + assert_eq!( + remote_selections, expected_selections, + "incorrect remote selections" + ); +} #[gpui::test] async fn test_multiple_handles_to_channel_buffer( diff --git a/crates/collab2/src/tests/editor_tests.rs b/crates/collab2/src/tests/editor_tests.rs index 6e84780466..31082f75c0 100644 --- a/crates/collab2/src/tests/editor_tests.rs +++ b/crates/collab2/src/tests/editor_tests.rs @@ -122,7 +122,6 @@ async fn test_host_disconnect( 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, @@ -216,7 +215,6 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( "}); } -todo!(editor) #[gpui::test(iterations = 10)] async fn test_collaborating_with_completion( executor: BackgroundExecutor, @@ -402,7 +400,7 @@ async fn test_collaborating_with_completion( ); }); } -todo!(editor) + #[gpui::test(iterations = 10)] async fn test_collaborating_with_code_actions( executor: BackgroundExecutor, @@ -621,7 +619,6 @@ async fn test_collaborating_with_code_actions( }); } -todo!(editor) #[gpui::test(iterations = 10)] async fn test_collaborating_with_renames( executor: BackgroundExecutor, @@ -815,7 +812,6 @@ async fn test_collaborating_with_renames( }) } -todo!(editor) #[gpui::test(iterations = 10)] async fn test_language_server_statuses( executor: BackgroundExecutor, @@ -1108,3 +1104,755 @@ async fn test_share_project( == 0 }); } + +#[gpui::test(iterations = 10)] +async fn test_on_input_format_from_host_to_guest( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&executor).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + // Open a file in an editor as the host. + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let window_a = cx_a.add_window(|_| EmptyView); + let editor_a = window_a.add_view(cx_a, |cx| { + Editor::for_buffer(buffer_a, Some(project_a.clone()), cx) + }); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + executor.run_until_parked(); + + // Receive an OnTypeFormatting request as the host's language server. + // Return some formattings from the host's language server. + fake_language_server.handle_request::( + |params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "~<".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + }])) + }, + ); + + // Open the buffer on the guest and see that the formattings worked + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + + // Type a on type formatting trigger character as the guest. + editor_a.update(cx_a, |editor, cx| { + cx.focus(&editor_a); + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(">", cx); + }); + + executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a>~< }") + }); + + // Undo should remove LSP edits first + editor_a.update(cx_a, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a>~< }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a> }"); + }); + executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a> }") + }); + + editor_a.update(cx_a, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a> }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a }"); + }); + executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a }") + }); +} + +#[gpui::test(iterations = 10)] +async fn test_on_input_format_from_guest_to_host( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&executor).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + // Open a file in an editor as the guest. + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let window_b = cx_b.add_window(|_| EmptyView); + let editor_b = window_b.add_view(cx_b, |cx| { + Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) + }); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + executor.run_until_parked(); + // Type a on type formatting trigger character as the guest. + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(":", cx); + cx.focus(&editor_b); + }); + + // Receive an OnTypeFormatting request as the host's language server. + // Return some formattings from the host's language server. + cx_a.foreground().start_waiting(); + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "~:".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + }])) + }) + .next() + .await + .unwrap(); + cx_a.foreground().finish_waiting(); + + // Open the buffer on the host and see that the formattings worked + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a:~: }") + }); + + // Undo should remove LSP edits first + editor_b.update(cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a:~: }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a: }"); + }); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a: }") + }); + + editor_b.update(cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a: }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a }"); + }); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a }") + }); +} + +#[gpui::test(iterations = 10)] +async fn test_mutual_editor_inlay_hint_cache_update( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&executor).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); + + // Client A opens a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Client B joins the project + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + cx_a.foreground().start_waiting(); + + // The host opens a rust file. + let _buffer_a = project_a + .update(cx_a, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Set up the language server to return an additional inlay hint on each request. + let edits_made = Arc::new(AtomicUsize::new(0)); + let closure_edits_made = Arc::clone(&edits_made); + fake_language_server + .handle_request::(move |params, _| { + let task_edits_made = Arc::clone(&closure_edits_made); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let edits_made = task_edits_made.load(atomic::Ordering::Acquire); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, edits_made as u32), + label: lsp::InlayHintLabel::String(edits_made.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await + .unwrap(); + + executor.run_until_parked(); + + let initial_edit = edits_made.load(atomic::Ordering::Acquire); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![initial_edit.to_string()], + extract_hint_labels(editor), + "Host should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Host editor update the cache version after every cache/view change", + ); + }); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![initial_edit.to_string()], + extract_hint_labels(editor), + "Client should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Guest editor update the cache version after every cache/view change" + ); + }); + + let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); + editor.handle_input(":", cx); + cx.focus(&editor_b); + }); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_client_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 2); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_client_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 2); + }); + + let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("a change to increment both buffers' versions", cx); + cx.focus(&editor_a); + }); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_host_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 3); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_host_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 3); + }); + + let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + fake_language_server + .request::(()) + .await + .expect("inlay refresh request failed"); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_special_edit_for_refresh.to_string()], + extract_hint_labels(editor), + "Host should react to /refresh LSP request" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 4, + "Host should accepted all edits and bump its cache version every time" + ); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_special_edit_for_refresh.to_string()], + extract_hint_labels(editor), + "Guest should get a /refresh LSP request propagated by host" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 4, + "Guest should accepted all edits and bump its cache version every time" + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_inlay_hint_refresh_is_forwarded( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&executor).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + }) + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + }); + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + cx_a.foreground().start_waiting(); + cx_b.foreground().start_waiting(); + + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let other_hints = Arc::new(AtomicBool::new(false)); + let fake_language_server = fake_language_servers.next().await.unwrap(); + let closure_other_hints = Arc::clone(&other_hints); + fake_language_server + .handle_request::(move |params, _| { + let task_other_hints = Arc::clone(&closure_other_hints); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let other_hints = task_other_hints.load(atomic::Ordering::Acquire); + let character = if other_hints { 0 } else { 2 }; + let label = if other_hints { + "other hint" + } else { + "initial hint" + }; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, character), + label: lsp::InlayHintLabel::String(label.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await + .unwrap(); + cx_a.foreground().finish_waiting(); + cx_b.foreground().finish_waiting(); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert!( + extract_hint_labels(editor).is_empty(), + "Host should get no hints due to them turned off" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 0, + "Turned off hints should not generate version updates" + ); + }); + + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["initial hint".to_string()], + extract_hint_labels(editor), + "Client should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Should update cache verison after first hints" + ); + }); + + other_hints.fetch_or(true, atomic::Ordering::Release); + fake_language_server + .request::(()) + .await + .expect("inlay refresh request failed"); + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert!( + extract_hint_labels(editor).is_empty(), + "Host should get nop hints due to them turned off, even after the /refresh" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 0, + "Turned off hints should not generate version updates, again" + ); + }); + + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["other hint".to_string()], + extract_hint_labels(editor), + "Guest should get a /refresh LSP request propagated by host despite host hints are off" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 2, + "Guest should accepted all edits and bump its cache version every time" + ); + }); +} + +fn extract_hint_labels(editor: &Editor) -> Vec { + let mut labels = Vec::new(); + for hint in editor.inlay_hint_cache().hints() { + match hint.label { + project::InlayHintLabel::String(s) => labels.push(s), + _ => unreachable!(), + } + } + labels +} diff --git a/crates/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs index f681e4877f..00c6c591b1 100644 --- a/crates/collab2/src/tests/integration_tests.rs +++ b/crates/collab2/src/tests/integration_tests.rs @@ -5718,757 +5718,754 @@ async fn test_join_call_after_screen_was_shared( }); } -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_on_input_format_from_host_to_guest( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); +#[gpui::test(iterations = 10)] +async fn test_on_input_format_from_host_to_guest( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&executor).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { -// first_trigger_character: ":".to_string(), -// more_trigger_character: Some(vec![">".to_string()]), -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; -// // Open a file in an editor as the host. -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// let window_a = cx_a.add_window(|_| EmptyView); -// let editor_a = window_a.add_view(cx_a, |cx| { -// Editor::for_buffer(buffer_a, Some(project_a.clone()), cx) -// }); + // Open a file in an editor as the host. + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let window_a = cx_a.add_window(|_| EmptyView); + let editor_a = window_a.add_view(cx_a, |cx| { + Editor::for_buffer(buffer_a, Some(project_a.clone()), cx) + }); -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// executor.run_until_parked(); + let fake_language_server = fake_language_servers.next().await.unwrap(); + executor.run_until_parked(); -// // Receive an OnTypeFormatting request as the host's language server. -// // Return some formattings from the host's language server. -// fake_language_server.handle_request::( -// |params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 14), -// ); + // Receive an OnTypeFormatting request as the host's language server. + // Return some formattings from the host's language server. + fake_language_server.handle_request::( + |params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); -// Ok(Some(vec![lsp::TextEdit { -// new_text: "~<".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// }])) -// }, -// ); + Ok(Some(vec![lsp::TextEdit { + new_text: "~<".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + }])) + }, + ); -// // Open the buffer on the guest and see that the formattings worked -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); + // Open the buffer on the guest and see that the formattings worked + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); -// // Type a on type formatting trigger character as the guest. -// editor_a.update(cx_a, |editor, cx| { -// cx.focus(&editor_a); -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(">", cx); -// }); + // Type a on type formatting trigger character as the guest. + editor_a.update(cx_a, |editor, cx| { + cx.focus(&editor_a); + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(">", cx); + }); -// executor.run_until_parked(); + executor.run_until_parked(); -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a>~< }") -// }); + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a>~< }") + }); -// // Undo should remove LSP edits first -// editor_a.update(cx_a, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a>~< }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a> }"); -// }); -// executor.run_until_parked(); + // Undo should remove LSP edits first + editor_a.update(cx_a, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a>~< }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a> }"); + }); + executor.run_until_parked(); -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a> }") -// }); + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a> }") + }); -// editor_a.update(cx_a, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a> }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a }"); -// }); -// executor.run_until_parked(); + editor_a.update(cx_a, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a> }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a }"); + }); + executor.run_until_parked(); -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a }") -// }); -// } + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a }") + }); +} -// #[gpui::test(iterations = 10)] -// async fn test_on_input_format_from_guest_to_host( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); +#[gpui::test(iterations = 10)] +async fn test_on_input_format_from_guest_to_host( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&executor).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { -// first_trigger_character: ":".to_string(), -// more_trigger_character: Some(vec![">".to_string()]), -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; -// // Open a file in an editor as the guest. -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// let window_b = cx_b.add_window(|_| EmptyView); -// let editor_b = window_b.add_view(cx_b, |cx| { -// Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) -// }); + // Open a file in an editor as the guest. + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let window_b = cx_b.add_window(|_| EmptyView); + let editor_b = window_b.add_view(cx_b, |cx| { + Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) + }); -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// executor.run_until_parked(); -// // Type a on type formatting trigger character as the guest. -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(":", cx); -// cx.focus(&editor_b); -// }); + let fake_language_server = fake_language_servers.next().await.unwrap(); + executor.run_until_parked(); + // Type a on type formatting trigger character as the guest. + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(":", cx); + cx.focus(&editor_b); + }); -// // Receive an OnTypeFormatting request as the host's language server. -// // Return some formattings from the host's language server. -// cx_a.foreground().start_waiting(); -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 14), -// ); + // Receive an OnTypeFormatting request as the host's language server. + // Return some formattings from the host's language server. + cx_a.foreground().start_waiting(); + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); -// Ok(Some(vec![lsp::TextEdit { -// new_text: "~:".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// }])) -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); + Ok(Some(vec![lsp::TextEdit { + new_text: "~:".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + }])) + }) + .next() + .await + .unwrap(); + cx_a.foreground().finish_waiting(); -// // Open the buffer on the host and see that the formattings worked -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); + // Open the buffer on the host and see that the formattings worked + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + executor.run_until_parked(); -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a:~: }") -// }); + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a:~: }") + }); -// // Undo should remove LSP edits first -// editor_b.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a:~: }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a: }"); -// }); -// executor.run_until_parked(); + // Undo should remove LSP edits first + editor_b.update(cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a:~: }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a: }"); + }); + executor.run_until_parked(); -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a: }") -// }); + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a: }") + }); -// editor_b.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a: }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a }"); -// }); -// executor.run_until_parked(); + editor_b.update(cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a: }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a }"); + }); + executor.run_until_parked(); -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a }") -// }); -// } + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a }") + }); +} -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_mutual_editor_inlay_hint_cache_update( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); +#[gpui::test(iterations = 10)] +async fn test_mutual_editor_inlay_hint_cache_update( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&executor).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); -// cx_a.update(editor::init); -// cx_b.update(editor::init); + cx_a.update(editor::init); + cx_b.update(editor::init); -// cx_a.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: false, -// show_other_hints: true, -// }) -// }); -// }); -// }); -// cx_b.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: false, -// show_other_hints: true, -// }) -// }); -// }); -// }); + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// client_a.language_registry().add(Arc::clone(&language)); -// client_b.language_registry().add(language); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); -// // Client A opens a project. -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); + // Client A opens a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); -// // Client B joins the project -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); + // Client B joins the project + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// cx_a.foreground().start_waiting(); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + cx_a.foreground().start_waiting(); -// // The host opens a rust file. -// let _buffer_a = project_a -// .update(cx_a, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// let editor_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); + // The host opens a rust file. + let _buffer_a = project_a + .update(cx_a, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); -// // Set up the language server to return an additional inlay hint on each request. -// let edits_made = Arc::new(AtomicUsize::new(0)); -// let closure_edits_made = Arc::clone(&edits_made); -// fake_language_server -// .handle_request::(move |params, _| { -// let task_edits_made = Arc::clone(&closure_edits_made); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let edits_made = task_edits_made.load(atomic::Ordering::Acquire); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, edits_made as u32), -// label: lsp::InlayHintLabel::String(edits_made.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await -// .unwrap(); + // Set up the language server to return an additional inlay hint on each request. + let edits_made = Arc::new(AtomicUsize::new(0)); + let closure_edits_made = Arc::clone(&edits_made); + fake_language_server + .handle_request::(move |params, _| { + let task_edits_made = Arc::clone(&closure_edits_made); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let edits_made = task_edits_made.load(atomic::Ordering::Acquire); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, edits_made as u32), + label: lsp::InlayHintLabel::String(edits_made.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await + .unwrap(); -// executor.run_until_parked(); + executor.run_until_parked(); -// let initial_edit = edits_made.load(atomic::Ordering::Acquire); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![initial_edit.to_string()], -// extract_hint_labels(editor), -// "Host should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Host editor update the cache version after every cache/view change", -// ); -// }); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); + let initial_edit = edits_made.load(atomic::Ordering::Acquire); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![initial_edit.to_string()], + extract_hint_labels(editor), + "Host should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Host editor update the cache version after every cache/view change", + ); + }); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![initial_edit.to_string()], -// extract_hint_labels(editor), -// "Client should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Guest editor update the cache version after every cache/view change" -// ); -// }); + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![initial_edit.to_string()], + extract_hint_labels(editor), + "Client should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Guest editor update the cache version after every cache/view change" + ); + }); -// let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); -// editor.handle_input(":", cx); -// cx.focus(&editor_b); -// }); + let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); + editor.handle_input(":", cx); + cx.focus(&editor_b); + }); -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_client_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 2); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_client_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 2); -// }); + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_client_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 2); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_client_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 2); + }); -// let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// editor_a.update(cx_a, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("a change to increment both buffers' versions", cx); -// cx.focus(&editor_a); -// }); + let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("a change to increment both buffers' versions", cx); + cx.focus(&editor_a); + }); -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_host_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 3); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_host_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 3); -// }); + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_host_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 3); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_host_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 3); + }); -// let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// fake_language_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); + let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + fake_language_server + .request::(()) + .await + .expect("inlay refresh request failed"); -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_special_edit_for_refresh.to_string()], -// extract_hint_labels(editor), -// "Host should react to /refresh LSP request" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 4, -// "Host should accepted all edits and bump its cache version every time" -// ); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_special_edit_for_refresh.to_string()], -// extract_hint_labels(editor), -// "Guest should get a /refresh LSP request propagated by host" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 4, -// "Guest should accepted all edits and bump its cache version every time" -// ); -// }); -// } + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_special_edit_for_refresh.to_string()], + extract_hint_labels(editor), + "Host should react to /refresh LSP request" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 4, + "Host should accepted all edits and bump its cache version every time" + ); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_special_edit_for_refresh.to_string()], + extract_hint_labels(editor), + "Guest should get a /refresh LSP request propagated by host" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 4, + "Guest should accepted all edits and bump its cache version every time" + ); + }); +} -//todo!(editor) -// #[gpui::test(iterations = 10)] -// async fn test_inlay_hint_refresh_is_forwarded( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); +#[gpui::test(iterations = 10)] +async fn test_inlay_hint_refresh_is_forwarded( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&executor).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); -// cx_a.update(editor::init); -// cx_b.update(editor::init); + cx_a.update(editor::init); + cx_b.update(editor::init); -// cx_a.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: false, -// show_type_hints: false, -// show_parameter_hints: false, -// show_other_hints: false, -// }) -// }); -// }); -// }); -// cx_b.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); -// }); -// }); + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + }) + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + }); + }); -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// client_a.language_registry().add(Arc::clone(&language)); -// client_b.language_registry().add(language); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// cx_a.foreground().start_waiting(); -// cx_b.foreground().start_waiting(); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + cx_a.foreground().start_waiting(); + cx_b.foreground().start_waiting(); -// let editor_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); -// let other_hints = Arc::new(AtomicBool::new(false)); -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// let closure_other_hints = Arc::clone(&other_hints); -// fake_language_server -// .handle_request::(move |params, _| { -// let task_other_hints = Arc::clone(&closure_other_hints); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let other_hints = task_other_hints.load(atomic::Ordering::Acquire); -// let character = if other_hints { 0 } else { 2 }; -// let label = if other_hints { -// "other hint" -// } else { -// "initial hint" -// }; -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, character), -// label: lsp::InlayHintLabel::String(label.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); -// cx_b.foreground().finish_waiting(); + let other_hints = Arc::new(AtomicBool::new(false)); + let fake_language_server = fake_language_servers.next().await.unwrap(); + let closure_other_hints = Arc::clone(&other_hints); + fake_language_server + .handle_request::(move |params, _| { + let task_other_hints = Arc::clone(&closure_other_hints); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let other_hints = task_other_hints.load(atomic::Ordering::Acquire); + let character = if other_hints { 0 } else { 2 }; + let label = if other_hints { + "other hint" + } else { + "initial hint" + }; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, character), + label: lsp::InlayHintLabel::String(label.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await + .unwrap(); + cx_a.foreground().finish_waiting(); + cx_b.foreground().finish_waiting(); -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert!( -// extract_hint_labels(editor).is_empty(), -// "Host should get no hints due to them turned off" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 0, -// "Turned off hints should not generate version updates" -// ); -// }); + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert!( + extract_hint_labels(editor).is_empty(), + "Host should get no hints due to them turned off" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 0, + "Turned off hints should not generate version updates" + ); + }); -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec!["initial hint".to_string()], -// extract_hint_labels(editor), -// "Client should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Should update cache verison after first hints" -// ); -// }); + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["initial hint".to_string()], + extract_hint_labels(editor), + "Client should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Should update cache verison after first hints" + ); + }); -// other_hints.fetch_or(true, atomic::Ordering::Release); -// fake_language_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert!( -// extract_hint_labels(editor).is_empty(), -// "Host should get nop hints due to them turned off, even after the /refresh" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 0, -// "Turned off hints should not generate version updates, again" -// ); -// }); + other_hints.fetch_or(true, atomic::Ordering::Release); + fake_language_server + .request::(()) + .await + .expect("inlay refresh request failed"); + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert!( + extract_hint_labels(editor).is_empty(), + "Host should get nop hints due to them turned off, even after the /refresh" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 0, + "Turned off hints should not generate version updates, again" + ); + }); -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec!["other hint".to_string()], -// extract_hint_labels(editor), -// "Guest should get a /refresh LSP request propagated by host despite host hints are off" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 2, -// "Guest should accepted all edits and bump its cache version every time" -// ); -// }); -// } + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["other hint".to_string()], + extract_hint_labels(editor), + "Guest should get a /refresh LSP request propagated by host despite host hints are off" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 2, + "Guest should accepted all edits and bump its cache version every time" + ); + }); +} -// fn extract_hint_labels(editor: &Editor) -> Vec { -// let mut labels = Vec::new(); -// for hint in editor.inlay_hint_cache().hints() { -// match hint.label { -// project::InlayHintLabel::String(s) => labels.push(s), -// _ => unreachable!(), -// } -// } -// labels -// } +fn extract_hint_labels(editor: &Editor) -> Vec { + let mut labels = Vec::new(); + for hint in editor.inlay_hint_cache().hints() { + match hint.label { + project::InlayHintLabel::String(s) => labels.push(s), + _ => unreachable!(), + } + } + labels +} diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 01b8d24801..898f53687b 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -10,9 +10,6 @@ use crate::{ 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, }; @@ -42,8 +39,8 @@ use workspace::{ fn test_edit_events(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456"); + let buffer = cx.build_model(|cx| { + let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456"); buffer.set_group_interval(Duration::from_secs(1)); buffer }); @@ -53,11 +50,8 @@ fn test_edit_events(cx: &mut TestAppContext) { .add_window({ let events = events.clone(); |cx| { - cx.subscribe(&cx.handle(), move |_, _, event, _| { - if matches!( - event, - Event::Edited | Event::BufferEdited | Event::DirtyChanged - ) { + cx.subscribe(cx.view(), move |_, _, event, _| { + if matches!(event, Event::Edited | Event::BufferEdited) { events.borrow_mut().push(("editor1", event.clone())); } }) @@ -65,16 +59,14 @@ fn test_edit_events(cx: &mut TestAppContext) { Editor::for_buffer(buffer.clone(), None, cx) } }) - .root(cx); + .root(cx) + .unwrap(); 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 - ) { + cx.subscribe(cx.view(), move |_, _, event, _| { + if matches!(event, Event::Edited | Event::BufferEdited) { events.borrow_mut().push(("editor2", event.clone())); } }) @@ -82,7 +74,8 @@ fn test_edit_events(cx: &mut TestAppContext) { Editor::for_buffer(buffer.clone(), None, cx) } }) - .root(cx); + .root(cx) + .unwrap(); assert_eq!(mem::take(&mut *events.borrow_mut()), []); // Mutating editor 1 will emit an `Edited` event only for that editor. @@ -93,8 +86,6 @@ fn test_edit_events(cx: &mut TestAppContext) { ("editor1", Event::Edited), ("editor1", Event::BufferEdited), ("editor2", Event::BufferEdited), - ("editor1", Event::DirtyChanged), - ("editor2", Event::DirtyChanged) ] ); @@ -365,7 +356,7 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { ); editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); + view.update_selection(DisplayPoint::new(3, 3), 0, Point::::zero(), cx); }); assert_eq!( @@ -374,7 +365,7 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { ); editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Point::::zero(), cx); }); assert_eq!( @@ -384,7 +375,7 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { editor.update(cx, |view, cx| { view.end_selection(cx); - view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); + view.update_selection(DisplayPoint::new(3, 3), 0, Point::::zero(), cx); }); assert_eq!( @@ -394,7 +385,7 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { 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); + view.update_selection(DisplayPoint::new(0, 0), 0, Point::::zero(), cx); }); assert_eq!( @@ -435,7 +426,7 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { }); view.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), 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)] @@ -444,7 +435,7 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { view.update(cx, |view, cx| { view.cancel(&Cancel, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), 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)] @@ -589,12 +580,12 @@ async fn test_navigation_history(cx: &mut TestAppContext) { 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); + 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); + 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(); @@ -643,11 +634,11 @@ fn test_cancel(cx: &mut TestAppContext) { 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.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.update_selection(DisplayPoint::new(0, 3), 0, Point::::zero(), cx); view.end_selection(cx); assert_eq!( view.selections.display_ranges(cx), diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index 631e9409d4..2d3be45a00 100644 --- a/crates/editor2/src/test.rs +++ b/crates/editor2/src/test.rs @@ -60,7 +60,7 @@ pub fn assert_text_with_selections( #[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) + Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx) } pub(crate) fn build_editor_with_project( @@ -68,5 +68,5 @@ pub(crate) fn build_editor_with_project( buffer: Model, cx: &mut ViewContext, ) -> Editor { - Editor::new(EditorMode::Full, buffer, Some(project), None, cx) + Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx) } diff --git a/crates/editor2/src/test/editor_lsp_test_context.rs b/crates/editor2/src/test/editor_lsp_test_context.rs index 5a6bdd2723..59fa420d48 100644 --- a/crates/editor2/src/test/editor_lsp_test_context.rs +++ b/crates/editor2/src/test/editor_lsp_test_context.rs @@ -1,297 +1,297 @@ -// use std::{ -// borrow::Cow, -// ops::{Deref, DerefMut, Range}, -// sync::Arc, -// }; +use std::{ + borrow::Cow, + ops::{Deref, DerefMut, Range}, + sync::Arc, +}; -// use anyhow::Result; +use anyhow::Result; -// use crate::{Editor, ToPoint}; -// use collections::HashSet; -// use futures::Future; -// use gpui::{json, View, ViewContext}; -// use indoc::indoc; -// use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; -// use lsp::{notification, request}; -// use multi_buffer::ToPointUtf16; -// use project::Project; -// use smol::stream::StreamExt; -// use workspace::{AppState, Workspace, WorkspaceHandle}; +use crate::{Editor, ToPoint}; +use collections::HashSet; +use futures::Future; +use gpui::{json, View, ViewContext}; +use indoc::indoc; +use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; +use lsp::{notification, request}; +use multi_buffer::ToPointUtf16; +use project::Project; +use smol::stream::StreamExt; +use workspace::{AppState, Workspace, WorkspaceHandle}; -// use super::editor_test_context::EditorTestContext; +use super::editor_test_context::EditorTestContext; -// pub struct EditorLspTestContext<'a> { -// pub cx: EditorTestContext<'a>, -// pub lsp: lsp::FakeLanguageServer, -// pub workspace: View, -// pub buffer_lsp_url: lsp::Url, -// } +pub struct EditorLspTestContext<'a> { + pub cx: EditorTestContext<'a>, + pub lsp: lsp::FakeLanguageServer, + pub workspace: View, + pub buffer_lsp_url: lsp::Url, +} -// impl<'a> EditorLspTestContext<'a> { -// pub async fn new( -// mut language: Language, -// capabilities: lsp::ServerCapabilities, -// cx: &'a mut gpui::TestAppContext, -// ) -> EditorLspTestContext<'a> { -// use json::json; +impl<'a> EditorLspTestContext<'a> { + pub async fn new( + mut language: Language, + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + use json::json; -// let app_state = cx.update(AppState::test); + let app_state = cx.update(AppState::test); -// cx.update(|cx| { -// language::init(cx); -// crate::init(cx); -// workspace::init(app_state.clone(), cx); -// Project::init_settings(cx); -// }); + cx.update(|cx| { + language::init(cx); + crate::init(cx); + workspace::init(app_state.clone(), cx); + Project::init_settings(cx); + }); -// let file_name = format!( -// "file.{}", -// language -// .path_suffixes() -// .first() -// .expect("language must have a path suffix for EditorLspTestContext") -// ); + let file_name = format!( + "file.{}", + language + .path_suffixes() + .first() + .expect("language must have a path suffix for EditorLspTestContext") + ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities, -// ..Default::default() -// })) -// .await; + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities, + ..Default::default() + })) + .await; -// let project = Project::test(app_state.fs.clone(), [], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let project = Project::test(app_state.fs.clone(), [], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// app_state -// .fs -// .as_fake() -// .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }})) -// .await; + app_state + .fs + .as_fake() + .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }})) + .await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// project -// .update(cx, |project, cx| { -// project.find_or_create_local_worktree("/root", true, cx) -// }) -// .await -// .unwrap(); -// cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) -// .await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root", true, cx) + }) + .await + .unwrap(); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; -// let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); -// let item = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path(file, None, true, cx) -// }) -// .await -// .expect("Could not open test file"); + let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); + let item = workspace + .update(cx, |workspace, cx| { + workspace.open_path(file, None, true, cx) + }) + .await + .expect("Could not open test file"); -// let editor = cx.update(|cx| { -// item.act_as::(cx) -// .expect("Opened test file wasn't an editor") -// }); -// editor.update(cx, |_, cx| cx.focus_self()); + let editor = cx.update(|cx| { + item.act_as::(cx) + .expect("Opened test file wasn't an editor") + }); + editor.update(cx, |_, cx| cx.focus_self()); -// let lsp = fake_servers.next().await.unwrap(); + let lsp = fake_servers.next().await.unwrap(); -// Self { -// cx: EditorTestContext { -// cx, -// window: window.into(), -// editor, -// }, -// lsp, -// workspace, -// buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(), -// } -// } + Self { + cx: EditorTestContext { + cx, + window: window.into(), + editor, + }, + lsp, + workspace, + buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(), + } + } -// pub async fn new_rust( -// capabilities: lsp::ServerCapabilities, -// cx: &'a mut gpui::TestAppContext, -// ) -> EditorLspTestContext<'a> { -// let language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_queries(LanguageQueries { -// indents: Some(Cow::from(indoc! {r#" -// [ -// ((where_clause) _ @end) -// (field_expression) -// (call_expression) -// (assignment_expression) -// (let_declaration) -// (let_chain) -// (await_expression) -// ] @indent + pub async fn new_rust( + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_queries(LanguageQueries { + indents: Some(Cow::from(indoc! {r#" + [ + ((where_clause) _ @end) + (field_expression) + (call_expression) + (assignment_expression) + (let_declaration) + (let_chain) + (await_expression) + ] @indent -// (_ "[" "]" @end) @indent -// (_ "<" ">" @end) @indent -// (_ "{" "}" @end) @indent -// (_ "(" ")" @end) @indent"#})), -// brackets: Some(Cow::from(indoc! {r#" -// ("(" @open ")" @close) -// ("[" @open "]" @close) -// ("{" @open "}" @close) -// ("<" @open ">" @close) -// ("\"" @open "\"" @close) -// (closure_parameters "|" @open "|" @close)"#})), -// ..Default::default() -// }) -// .expect("Could not parse queries"); + (_ "[" "]" @end) @indent + (_ "<" ">" @end) @indent + (_ "{" "}" @end) @indent + (_ "(" ")" @end) @indent"#})), + brackets: Some(Cow::from(indoc! {r#" + ("(" @open ")" @close) + ("[" @open "]" @close) + ("{" @open "}" @close) + ("<" @open ">" @close) + ("\"" @open "\"" @close) + (closure_parameters "|" @open "|" @close)"#})), + ..Default::default() + }) + .expect("Could not parse queries"); -// Self::new(language, capabilities, cx).await -// } + Self::new(language, capabilities, cx).await + } -// pub async fn new_typescript( -// capabilities: lsp::ServerCapabilities, -// cx: &'a mut gpui::TestAppContext, -// ) -> EditorLspTestContext<'a> { -// let mut word_characters: HashSet = Default::default(); -// word_characters.insert('$'); -// word_characters.insert('#'); -// let language = Language::new( -// LanguageConfig { -// name: "Typescript".into(), -// path_suffixes: vec!["ts".to_string()], -// brackets: language::BracketPairConfig { -// pairs: vec![language::BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: true, -// newline: true, -// }], -// disabled_scopes_by_bracket_ix: Default::default(), -// }, -// word_characters, -// ..Default::default() -// }, -// Some(tree_sitter_typescript::language_typescript()), -// ) -// .with_queries(LanguageQueries { -// brackets: Some(Cow::from(indoc! {r#" -// ("(" @open ")" @close) -// ("[" @open "]" @close) -// ("{" @open "}" @close) -// ("<" @open ">" @close) -// ("\"" @open "\"" @close)"#})), -// indents: Some(Cow::from(indoc! {r#" -// [ -// (call_expression) -// (assignment_expression) -// (member_expression) -// (lexical_declaration) -// (variable_declaration) -// (assignment_expression) -// (if_statement) -// (for_statement) -// ] @indent + pub async fn new_typescript( + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + let mut word_characters: HashSet = Default::default(); + word_characters.insert('$'); + word_characters.insert('#'); + let language = Language::new( + LanguageConfig { + name: "Typescript".into(), + path_suffixes: vec!["ts".to_string()], + brackets: language::BracketPairConfig { + pairs: vec![language::BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }], + disabled_scopes_by_bracket_ix: Default::default(), + }, + word_characters, + ..Default::default() + }, + Some(tree_sitter_typescript::language_typescript()), + ) + .with_queries(LanguageQueries { + brackets: Some(Cow::from(indoc! {r#" + ("(" @open ")" @close) + ("[" @open "]" @close) + ("{" @open "}" @close) + ("<" @open ">" @close) + ("\"" @open "\"" @close)"#})), + indents: Some(Cow::from(indoc! {r#" + [ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) + ] @indent -// (_ "[" "]" @end) @indent -// (_ "<" ">" @end) @indent -// (_ "{" "}" @end) @indent -// (_ "(" ")" @end) @indent -// "#})), -// ..Default::default() -// }) -// .expect("Could not parse queries"); + (_ "[" "]" @end) @indent + (_ "<" ">" @end) @indent + (_ "{" "}" @end) @indent + (_ "(" ")" @end) @indent + "#})), + ..Default::default() + }) + .expect("Could not parse queries"); -// Self::new(language, capabilities, cx).await -// } + Self::new(language, capabilities, cx).await + } -// // Constructs lsp range using a marked string with '[', ']' range delimiters -// pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { -// let ranges = self.ranges(marked_text); -// self.to_lsp_range(ranges[0].clone()) -// } + // Constructs lsp range using a marked string with '[', ']' range delimiters + pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { + let ranges = self.ranges(marked_text); + self.to_lsp_range(ranges[0].clone()) + } -// pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { -// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); -// let start_point = range.start.to_point(&snapshot.buffer_snapshot); -// let end_point = range.end.to_point(&snapshot.buffer_snapshot); + pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let start_point = range.start.to_point(&snapshot.buffer_snapshot); + let end_point = range.end.to_point(&snapshot.buffer_snapshot); -// self.editor(|editor, cx| { -// let buffer = editor.buffer().read(cx); -// let start = point_to_lsp( -// buffer -// .point_to_buffer_offset(start_point, cx) -// .unwrap() -// .1 -// .to_point_utf16(&buffer.read(cx)), -// ); -// let end = point_to_lsp( -// buffer -// .point_to_buffer_offset(end_point, cx) -// .unwrap() -// .1 -// .to_point_utf16(&buffer.read(cx)), -// ); + self.editor(|editor, cx| { + let buffer = editor.buffer().read(cx); + let start = point_to_lsp( + buffer + .point_to_buffer_offset(start_point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ); + let end = point_to_lsp( + buffer + .point_to_buffer_offset(end_point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ); -// lsp::Range { start, end } -// }) -// } + lsp::Range { start, end } + }) + } -// pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { -// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); -// let point = offset.to_point(&snapshot.buffer_snapshot); + pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let point = offset.to_point(&snapshot.buffer_snapshot); -// self.editor(|editor, cx| { -// let buffer = editor.buffer().read(cx); -// point_to_lsp( -// buffer -// .point_to_buffer_offset(point, cx) -// .unwrap() -// .1 -// .to_point_utf16(&buffer.read(cx)), -// ) -// }) -// } + self.editor(|editor, cx| { + let buffer = editor.buffer().read(cx); + point_to_lsp( + buffer + .point_to_buffer_offset(point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ) + }) + } -// pub fn update_workspace(&mut self, update: F) -> T -// where -// F: FnOnce(&mut Workspace, &mut ViewContext) -> T, -// { -// self.workspace.update(self.cx.cx, update) -// } + pub fn update_workspace(&mut self, update: F) -> T + where + F: FnOnce(&mut Workspace, &mut ViewContext) -> T, + { + self.workspace.update(self.cx.cx, update) + } -// pub fn handle_request( -// &self, -// mut handler: F, -// ) -> futures::channel::mpsc::UnboundedReceiver<()> -// where -// T: 'static + request::Request, -// T::Params: 'static + Send, -// F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut, -// Fut: 'static + Send + Future>, -// { -// let url = self.buffer_lsp_url.clone(); -// self.lsp.handle_request::(move |params, cx| { -// let url = url.clone(); -// handler(url, params, cx) -// }) -// } + pub fn handle_request( + &self, + mut handler: F, + ) -> futures::channel::mpsc::UnboundedReceiver<()> + where + T: 'static + request::Request, + T::Params: 'static + Send, + F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut, + Fut: 'static + Send + Future>, + { + let url = self.buffer_lsp_url.clone(); + self.lsp.handle_request::(move |params, cx| { + let url = url.clone(); + handler(url, params, cx) + }) + } -// pub fn notify(&self, params: T::Params) { -// self.lsp.notify::(params); -// } -// } + pub fn notify(&self, params: T::Params) { + self.lsp.notify::(params); + } +} -// impl<'a> Deref for EditorLspTestContext<'a> { -// type Target = EditorTestContext<'a>; +impl<'a> Deref for EditorLspTestContext<'a> { + type Target = EditorTestContext<'a>; -// fn deref(&self) -> &Self::Target { -// &self.cx -// } -// } + fn deref(&self) -> &Self::Target { + &self.cx + } +} -// impl<'a> DerefMut for EditorLspTestContext<'a> { -// fn deref_mut(&mut self) -> &mut Self::Target { -// &mut self.cx -// } -// } +impl<'a> DerefMut for EditorLspTestContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx + } +} diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index cd0f8e5fbf..c609b8781a 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -357,8 +357,7 @@ async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncApp } else { cx.update(|cx| { workspace::open_new(app_state, cx, |workspace, cx| { - // todo!(editor) - // Editor::new_file(workspace, &Default::default(), cx) + Editor::new_file(workspace, &Default::default(), cx) }) .detach(); })?; From 86630bbe590cebea8225d17357b6f547d21329d3 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 8 Nov 2023 22:16:08 -0800 Subject: [PATCH 004/126] Add extra clone --- crates/zed2/src/zed2.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 7368d3a5ef..de985496c8 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -55,7 +55,7 @@ pub fn initialize_workspace( ) -> Task> { cx.spawn(|mut cx| async move { workspace_handle.update(&mut cx, |workspace, cx| { - let workspace_handle = cx.view(); + let workspace_handle = cx.view().clone(); cx.subscribe(&workspace_handle, { move |workspace, _, event, cx| { if let workspace::Event::PaneAdded(pane) = event { From 43eb7f28d1f9817dbc7b34a8ccd9726a442f3460 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 8 Nov 2023 23:16:04 -0800 Subject: [PATCH 005/126] checkpoint --- crates/client2/src/test.rs | 2 +- crates/collab2/src/db/tests/db_tests.rs | 2 +- crates/collab2/src/tests/editor_tests.rs | 4 +- crates/collab2/src/tests/integration_tests.rs | 752 ----------------- crates/collab2/src/tests/test_server.rs | 4 +- crates/copilot2/src/copilot2.rs | 43 +- crates/editor2/src/editor_tests.rs | 757 ++++++++---------- crates/editor2/src/test.rs | 2 + .../editor2/src/test/editor_test_context.rs | 2 +- crates/gpui2/src/app.rs | 17 +- crates/gpui2/src/app/async_context.rs | 22 +- crates/gpui2/src/app/model_context.rs | 13 +- crates/gpui2/src/app/test_context.rs | 15 +- crates/gpui2/src/color.rs | 33 + crates/gpui2/src/gpui2.rs | 10 +- crates/gpui2/src/window.rs | 78 +- crates/language2/src/language2.rs | 4 +- crates/prettier2/src/prettier2.rs | 10 +- crates/project2/src/project2.rs | 2 +- crates/project2/src/project_tests.rs | 68 +- crates/rpc2/src/peer.rs | 20 +- crates/workspace2/src/workspace2.rs | 38 +- 22 files changed, 591 insertions(+), 1307 deletions(-) diff --git a/crates/client2/src/test.rs b/crates/client2/src/test.rs index 5462799103..086cbd570e 100644 --- a/crates/client2/src/test.rs +++ b/crates/client2/src/test.rs @@ -36,7 +36,7 @@ impl FakeServer { peer: Peer::new(0), state: Default::default(), user_id: client_user_id, - executor: cx.executor().clone(), + executor: cx.executor(), }; client diff --git a/crates/collab2/src/db/tests/db_tests.rs b/crates/collab2/src/db/tests/db_tests.rs index 98d1fee8fa..1f825efd74 100644 --- a/crates/collab2/src/db/tests/db_tests.rs +++ b/crates/collab2/src/db/tests/db_tests.rs @@ -510,7 +510,7 @@ fn test_fuzzy_like_string() { #[gpui::test] async fn test_fuzzy_search_users(cx: &mut TestAppContext) { - let test_db = TestDb::postgres(cx.executor().clone()); + let test_db = TestDb::postgres(cx.executor()); let db = test_db.db(); for (i, github_login) in [ "California", diff --git a/crates/collab2/src/tests/editor_tests.rs b/crates/collab2/src/tests/editor_tests.rs index 31082f75c0..5d1378a882 100644 --- a/crates/collab2/src/tests/editor_tests.rs +++ b/crates/collab2/src/tests/editor_tests.rs @@ -48,9 +48,9 @@ async fn test_host_disconnect( assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - let window_b = + let workspace_b = cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); - let workspace_b = window_b.root(cx_b); + let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "b.txt"), None, true, cx) diff --git a/crates/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs index 00c6c591b1..121a98c1d2 100644 --- a/crates/collab2/src/tests/integration_tests.rs +++ b/crates/collab2/src/tests/integration_tests.rs @@ -5717,755 +5717,3 @@ async fn test_join_call_after_screen_was_shared( ); }); } - -#[gpui::test(iterations = 10)] -async fn test_on_input_format_from_host_to_guest( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(&executor).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { - first_trigger_character: ":".to_string(), - more_trigger_character: Some(vec![">".to_string()]), - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a }", - "other.rs": "// Test file", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - - // Open a file in an editor as the host. - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - let window_a = cx_a.add_window(|_| EmptyView); - let editor_a = window_a.add_view(cx_a, |cx| { - Editor::for_buffer(buffer_a, Some(project_a.clone()), cx) - }); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - executor.run_until_parked(); - - // Receive an OnTypeFormatting request as the host's language server. - // Return some formattings from the host's language server. - fake_language_server.handle_request::( - |params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 14), - ); - - Ok(Some(vec![lsp::TextEdit { - new_text: "~<".to_string(), - range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), - }])) - }, - ); - - // Open the buffer on the guest and see that the formattings worked - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - - // Type a on type formatting trigger character as the guest. - editor_a.update(cx_a, |editor, cx| { - cx.focus(&editor_a); - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(">", cx); - }); - - executor.run_until_parked(); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a>~< }") - }); - - // Undo should remove LSP edits first - editor_a.update(cx_a, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a>~< }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a> }"); - }); - executor.run_until_parked(); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a> }") - }); - - editor_a.update(cx_a, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a> }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a }"); - }); - executor.run_until_parked(); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a }") - }); -} - -#[gpui::test(iterations = 10)] -async fn test_on_input_format_from_guest_to_host( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(&executor).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { - first_trigger_character: ":".to_string(), - more_trigger_character: Some(vec![">".to_string()]), - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a }", - "other.rs": "// Test file", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - - // Open a file in an editor as the guest. - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - let window_b = cx_b.add_window(|_| EmptyView); - let editor_b = window_b.add_view(cx_b, |cx| { - Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) - }); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - executor.run_until_parked(); - // Type a on type formatting trigger character as the guest. - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(":", cx); - cx.focus(&editor_b); - }); - - // Receive an OnTypeFormatting request as the host's language server. - // Return some formattings from the host's language server. - cx_a.foreground().start_waiting(); - fake_language_server - .handle_request::(|params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 14), - ); - - Ok(Some(vec![lsp::TextEdit { - new_text: "~:".to_string(), - range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), - }])) - }) - .next() - .await - .unwrap(); - cx_a.foreground().finish_waiting(); - - // Open the buffer on the host and see that the formattings worked - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a:~: }") - }); - - // Undo should remove LSP edits first - editor_b.update(cx_b, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a:~: }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a: }"); - }); - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a: }") - }); - - editor_b.update(cx_b, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a: }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a }"); - }); - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a }") - }); -} - -#[gpui::test(iterations = 10)] -async fn test_mutual_editor_inlay_hint_cache_update( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(&executor).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - cx_a.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: false, - show_other_hints: true, - }) - }); - }); - }); - cx_b.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: false, - show_other_hints: true, - }) - }); - }); - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - let language = Arc::new(language); - client_a.language_registry().add(Arc::clone(&language)); - client_b.language_registry().add(language); - - // Client A opens a project. - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", - "other.rs": "// Test file", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - // Client B joins the project - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - cx_a.foreground().start_waiting(); - - // The host opens a rust file. - let _buffer_a = project_a - .update(cx_a, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - let fake_language_server = fake_language_servers.next().await.unwrap(); - let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Set up the language server to return an additional inlay hint on each request. - let edits_made = Arc::new(AtomicUsize::new(0)); - let closure_edits_made = Arc::clone(&edits_made); - fake_language_server - .handle_request::(move |params, _| { - let task_edits_made = Arc::clone(&closure_edits_made); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - let edits_made = task_edits_made.load(atomic::Ordering::Acquire); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, edits_made as u32), - label: lsp::InlayHintLabel::String(edits_made.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await - .unwrap(); - - executor.run_until_parked(); - - let initial_edit = edits_made.load(atomic::Ordering::Acquire); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![initial_edit.to_string()], - extract_hint_labels(editor), - "Host should get its first hints when opens an editor" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Host editor update the cache version after every cache/view change", - ); - }); - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![initial_edit.to_string()], - extract_hint_labels(editor), - "Client should get its first hints when opens an editor" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Guest editor update the cache version after every cache/view change" - ); - }); - - let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); - editor.handle_input(":", cx); - cx.focus(&editor_b); - }); - - executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![after_client_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 2); - }); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![after_client_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 2); - }); - - let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - editor_a.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input("a change to increment both buffers' versions", cx); - cx.focus(&editor_a); - }); - - executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![after_host_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 3); - }); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![after_host_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 3); - }); - - let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - fake_language_server - .request::(()) - .await - .expect("inlay refresh request failed"); - - executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![after_special_edit_for_refresh.to_string()], - extract_hint_labels(editor), - "Host should react to /refresh LSP request" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 4, - "Host should accepted all edits and bump its cache version every time" - ); - }); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![after_special_edit_for_refresh.to_string()], - extract_hint_labels(editor), - "Guest should get a /refresh LSP request propagated by host" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 4, - "Guest should accepted all edits and bump its cache version every time" - ); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_inlay_hint_refresh_is_forwarded( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(&executor).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - cx_a.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: false, - show_type_hints: false, - show_parameter_hints: false, - show_other_hints: false, - }) - }); - }); - }); - cx_b.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - }) - }); - }); - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - let language = Arc::new(language); - client_a.language_registry().add(Arc::clone(&language)); - client_b.language_registry().add(language); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", - "other.rs": "// Test file", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - cx_a.foreground().start_waiting(); - cx_b.foreground().start_waiting(); - - let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - let other_hints = Arc::new(AtomicBool::new(false)); - let fake_language_server = fake_language_servers.next().await.unwrap(); - let closure_other_hints = Arc::clone(&other_hints); - fake_language_server - .handle_request::(move |params, _| { - let task_other_hints = Arc::clone(&closure_other_hints); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - let other_hints = task_other_hints.load(atomic::Ordering::Acquire); - let character = if other_hints { 0 } else { 2 }; - let label = if other_hints { - "other hint" - } else { - "initial hint" - }; - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, character), - label: lsp::InlayHintLabel::String(label.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await - .unwrap(); - cx_a.foreground().finish_waiting(); - cx_b.foreground().finish_waiting(); - - executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert!( - extract_hint_labels(editor).is_empty(), - "Host should get no hints due to them turned off" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 0, - "Turned off hints should not generate version updates" - ); - }); - - executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec!["initial hint".to_string()], - extract_hint_labels(editor), - "Client should get its first hints when opens an editor" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Should update cache verison after first hints" - ); - }); - - other_hints.fetch_or(true, atomic::Ordering::Release); - fake_language_server - .request::(()) - .await - .expect("inlay refresh request failed"); - executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert!( - extract_hint_labels(editor).is_empty(), - "Host should get nop hints due to them turned off, even after the /refresh" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 0, - "Turned off hints should not generate version updates, again" - ); - }); - - executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec!["other hint".to_string()], - extract_hint_labels(editor), - "Guest should get a /refresh LSP request propagated by host despite host hints are off" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 2, - "Guest should accepted all edits and bump its cache version every time" - ); - }); -} - -fn extract_hint_labels(editor: &Editor) -> Vec { - let mut labels = Vec::new(); - for hint in editor.inlay_hint_cache().hints() { - match hint.label { - project::InlayHintLabel::String(s) => labels.push(s), - _ => unreachable!(), - } - } - labels -} diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index d0ab917d68..1b4d8945ae 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -208,11 +208,11 @@ impl TestServer { }) }); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx)); let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); let mut language_registry = LanguageRegistry::test(); - language_registry.set_executor(cx.executor().clone()); + language_registry.set_executor(cx.executor()); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index 2daf2fec12..53d802dd03 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -351,28 +351,29 @@ impl Copilot { } } - // #[cfg(any(test, feature = "test-support"))] - // pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle, lsp::FakeLanguageServer) { - // use node_runtime::FakeNodeRuntime; + #[cfg(any(test, feature = "test-support"))] + pub fn fake(cx: &mut gpui::TestAppContext) -> (Model, lsp::FakeLanguageServer) { + use node_runtime::FakeNodeRuntime; - // let (server, fake_server) = - // LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); - // let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); - // let node_runtime = FakeNodeRuntime::new(); - // let this = cx.add_model(|_| Self { - // server_id: LanguageServerId(0), - // http: http.clone(), - // node_runtime, - // server: CopilotServer::Running(RunningCopilotServer { - // name: LanguageServerName(Arc::from("copilot")), - // lsp: Arc::new(server), - // sign_in_status: SignInStatus::Authorized, - // registered_buffers: Default::default(), - // }), - // buffers: Default::default(), - // }); - // (this, fake_server) - // } + let (server, fake_server) = + LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); + let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); + let node_runtime = FakeNodeRuntime::new(); + let this = cx.build_model(|cx| Self { + server_id: LanguageServerId(0), + http: http.clone(), + node_runtime, + server: CopilotServer::Running(RunningCopilotServer { + name: LanguageServerName(Arc::from("copilot")), + lsp: Arc::new(server), + sign_in_status: SignInStatus::Authorized, + registered_buffers: Default::default(), + }), + _subscription: cx.on_app_quit(Self::shutdown_language_server), + buffers: Default::default(), + }); + (this, fake_server) + } fn start_language_server( new_server_id: LanguageServerId, diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 898f53687b..08c44d7682 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -11,7 +11,7 @@ use drag_and_drop::DragAndDrop; use futures::StreamExt; use gpui::{ serde_json::{self, json}, - TestAppContext, + TestAppContext, WindowOptions, }; use indoc::indoc; use language::{ @@ -31,7 +31,7 @@ use util::{ test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, }; use workspace::{ - item::{FollowableItem, Item, ItemHandle}, + item::{FollowableEvents, FollowableItem, Item, ItemHandle}, NavigationEntry, ViewId, }; @@ -46,36 +46,32 @@ fn test_edit_events(cx: &mut TestAppContext) { }); let events = Rc::new(RefCell::new(Vec::new())); - let editor1 = cx - .add_window({ - let events = events.clone(); - |cx| { - cx.subscribe(cx.view(), move |_, _, event, _| { - if matches!(event, Event::Edited | Event::BufferEdited) { - events.borrow_mut().push(("editor1", event.clone())); - } - }) - .detach(); - Editor::for_buffer(buffer.clone(), None, cx) - } - }) - .root(cx) - .unwrap(); - let editor2 = cx - .add_window({ - let events = events.clone(); - |cx| { - cx.subscribe(cx.view(), move |_, _, event, _| { - if matches!(event, Event::Edited | Event::BufferEdited) { - events.borrow_mut().push(("editor2", event.clone())); - } - }) - .detach(); - Editor::for_buffer(buffer.clone(), None, cx) - } - }) - .root(cx) - .unwrap(); + let editor1 = cx.add_window({ + let events = events.clone(); + |cx| { + cx.subscribe(cx.view(), move |_, _, event, _| { + if matches!(event, Event::Edited | Event::BufferEdited) { + events.borrow_mut().push(("editor1", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }); + + let editor2 = cx.add_window({ + let events = events.clone(); + |cx| { + cx.subscribe(cx.view(), move |_, _, event, _| { + if matches!(event, Event::Edited | Event::BufferEdited) { + events.borrow_mut().push(("editor2", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); // Mutating editor 1 will emit an `Edited` event only for that editor. @@ -108,8 +104,6 @@ fn test_edit_events(cx: &mut TestAppContext) { ("editor1", Event::Edited), ("editor1", Event::BufferEdited), ("editor2", Event::BufferEdited), - ("editor1", Event::DirtyChanged), - ("editor2", Event::DirtyChanged), ] ); @@ -121,8 +115,6 @@ fn test_edit_events(cx: &mut TestAppContext) { ("editor1", Event::Edited), ("editor1", Event::BufferEdited), ("editor2", Event::BufferEdited), - ("editor1", Event::DirtyChanged), - ("editor2", Event::DirtyChanged), ] ); @@ -134,8 +126,6 @@ fn test_edit_events(cx: &mut TestAppContext) { ("editor2", Event::Edited), ("editor1", Event::BufferEdited), ("editor2", Event::BufferEdited), - ("editor1", Event::DirtyChanged), - ("editor2", Event::DirtyChanged), ] ); @@ -147,8 +137,6 @@ fn test_edit_events(cx: &mut TestAppContext) { ("editor2", Event::Edited), ("editor1", Event::BufferEdited), ("editor2", Event::BufferEdited), - ("editor1", Event::DirtyChanged), - ("editor2", Event::DirtyChanged), ] ); @@ -166,12 +154,10 @@ 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 buffer = cx.build_model(|cx| language::Buffer::new(0, cx.entity_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); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx)); editor.update(cx, |editor, cx| { editor.start_transaction_at(now, cx); @@ -238,14 +224,14 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { 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"); + let buffer = cx.build_model(|cx| { + let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "abcde"); // Ensure automatic grouping doesn't occur. buffer.set_group_interval(Duration::ZERO); buffer }); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); cx.add_window(|cx| { let mut editor = build_editor(buffer.clone(), cx); @@ -341,55 +327,64 @@ fn test_ime_composition(cx: &mut TestAppContext) { 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); + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); + build_editor(buffer, 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)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] ); editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Point::::zero(), cx); + view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] ); editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(1, 1), 0, Point::::zero(), cx); + view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [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); + view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [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); + view.update_selection(DisplayPoint::new(0, 0), 0, gpui::Point::::zero(), cx); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [ DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) @@ -401,7 +396,9 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] ); } @@ -410,12 +407,10 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); @@ -426,7 +421,7 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { }); view.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Point::::zero(), cx); + view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); assert_eq!( view.selections.display_ranges(cx), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] @@ -435,7 +430,7 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { view.update(cx, |view, cx| { view.cancel(&Cancel, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, Point::::zero(), cx); + view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); assert_eq!( view.selections.display_ranges(cx), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] @@ -458,12 +453,10 @@ fn test_clone(cx: &mut TestAppContext) { true, ); - let editor = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple(&text, cx); - build_editor(buffer, cx) - }) - .root(cx); + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&text, cx); + build_editor(buffer, cx) + }); editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); @@ -477,11 +470,9 @@ fn test_clone(cx: &mut TestAppContext) { ); }); - let cloned_editor = editor - .update(cx, |editor, cx| { - cx.add_window(Default::default(), |cx| editor.clone(cx)) - }) - .root(cx); + let cloned_editor = editor.update(cx, |editor, cx| { + cx.add_window(Default::default(), |cx| editor.clone(cx)) + }); let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); @@ -513,11 +504,12 @@ async fn test_navigation_history(cx: &mut TestAppContext) { cx.set_global(DragAndDrop::::default()); use workspace::item::Item; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); + let workspace = window; 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); @@ -614,7 +606,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { ); assert_eq!( editor.scroll_position(cx), - vec2f(0., editor.max_point(cx).row() as f32) + Point::new(0., editor.max_point(cx).row() as f32) ); editor @@ -625,20 +617,28 @@ async fn test_navigation_history(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, 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.update_selection( + DisplayPoint::new(1, 1), + 0, + gpui::Point::::zero(), + cx, + ); view.end_selection(cx); view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 3), 0, Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(0, 3), + 0, + gpui::Point::::zero(), + cx, + ); view.end_selection(cx); assert_eq!( view.selections.display_ranges(cx), @@ -670,10 +670,9 @@ fn test_cancel(cx: &mut TestAppContext) { fn test_fold_action(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple( - &" + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple( + &" impl Foo { // Hello! @@ -690,12 +689,11 @@ fn test_fold_action(cx: &mut TestAppContext) { } } " - .unindent(), - cx, - ); - build_editor(buffer.clone(), cx) - }) - .root(cx); + .unindent(), + cx, + ); + build_editor(buffer.clone(), cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -763,9 +761,7 @@ 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); + let view = cx.add_window(|cx| build_editor(buffer.clone(), cx)); buffer.update(cx, |buffer, cx| { buffer.edit( @@ -840,12 +836,10 @@ fn test_move_cursor(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); + build_editor(buffer.clone(), cx) + }); assert_eq!('ⓐ'.len_utf8(), 3); assert_eq!('α'.len_utf8(), 2); @@ -958,12 +952,10 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); + build_editor(buffer.clone(), cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); @@ -1010,12 +1002,10 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\n def", cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -1175,12 +1165,10 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -1229,13 +1217,10 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx - .add_window(|cx| { - let buffer = - MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); - build_editor(buffer, cx) - }) - .root(cx); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.set_wrap_width(Some(140.), cx); @@ -1293,7 +1278,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon 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); + window.simulate_resize(Point::new(100., 4. * line_height), &mut cx); cx.set_state( &r#"ˇone @@ -1392,7 +1377,7 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { 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); + window.simulate_resize(Point::new(1000., 4. * line_height + 0.5), &mut cx); cx.set_state( &r#"ˇone @@ -1409,18 +1394,18 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { ); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)); + assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 0.)); editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 3.)); editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.)); + assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 6.)); editor.scroll_screen(&ScrollAmount::Page(-1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 3.)); editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.)); + assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 1.)); editor.scroll_screen(&ScrollAmount::Page(0.5), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 3.)); }); } @@ -1435,7 +1420,7 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { }); let window = cx.window; - window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx); + window.simulate_resize(Point::new(1000., 6.0 * line_height), &mut cx); cx.set_state( &r#"ˇone @@ -1451,7 +1436,7 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { "#, ); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0)); + assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 0.0)); }); // Add a cursor below the visible area. Since both cursors cannot fit @@ -1466,7 +1451,7 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { }) }); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0)); + assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 3.0)); }); // Move down. The editor cursor scrolls down to track the newest cursor. @@ -1474,7 +1459,7 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { editor.move_down(&Default::default(), cx); }); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0)); + assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 4.0)); }); // Add a cursor above the visible area. Since both cursors fit on screen, @@ -1488,7 +1473,7 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { }) }); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0)); + assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 1.0)); }); } @@ -1499,7 +1484,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { 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); + window.simulate_resize(Point::new(100., 4. * line_height), &mut cx); cx.set_state( &r#" @@ -1623,12 +1608,10 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("one two three four", cx); + build_editor(buffer.clone(), cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -1661,12 +1644,10 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); + build_editor(buffer.clone(), cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -1686,10 +1667,9 @@ fn test_newline(cx: &mut TestAppContext) { fn test_newline_with_old_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple( - " + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple( + " a b( X @@ -1698,20 +1678,19 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { 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); + .unindent() + .as_str(), + cx, + ); + let mut editor = build_editor(buffer.clone(), cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(2, 4)..Point::new(2, 5), + Point::new(5, 4)..Point::new(5, 5), + ]) + }); + editor + }); editor.update(cx, |editor, cx| { // Edit the buffer directly, deleting ranges surrounding the editor's selections @@ -1916,14 +1895,12 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) { 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); + 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 + }); editor.update(cx, |editor, cx| { // Edit the buffer directly, deleting ranges surrounding the editor's selections @@ -2271,14 +2248,14 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { 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 toml_buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n").with_language(toml_language, cx) }); - let rust_buffer = cx.add_model(|cx| { - Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") + let rust_buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), "const c: usize = 3;\n") .with_language(rust_language, cx) }); - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.build_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( toml_buffer.clone(), @@ -2432,12 +2409,10 @@ async fn test_delete(cx: &mut gpui::TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -2457,12 +2432,10 @@ fn test_delete_line(cx: &mut TestAppContext) { ); }); - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }) - .root(cx); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) @@ -2867,12 +2840,10 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -2895,12 +2866,10 @@ fn test_duplicate_line(cx: &mut TestAppContext) { ); }); - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }) - .root(cx); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -2924,12 +2893,10 @@ fn test_duplicate_line(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.fold_ranges( vec![ @@ -3025,12 +2992,10 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { 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); + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + build_editor(buffer, cx) + }); editor.update(cx, |editor, cx| { let snapshot = editor.buffer.read(cx).snapshot(cx); editor.insert_blocks( @@ -3345,12 +3310,10 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.select_all(&SelectAll, cx); assert_eq!( @@ -3364,12 +3327,10 @@ fn test_select_all(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -3413,12 +3374,10 @@ fn test_select_line(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.fold_ranges( vec![ @@ -3486,12 +3445,10 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); + build_editor(buffer, cx) + }); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -3786,10 +3743,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { "# .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); + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -3950,10 +3908,11 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { 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); + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)); editor .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; @@ -4514,10 +4473,11 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { "# .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); + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -4663,10 +4623,11 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { "# .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); + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)); editor .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -4756,7 +4717,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { ); let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + let editor = cx.add_window(|cx| build_editor(buffer, cx)); editor.update(cx, |editor, cx| { let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); @@ -4872,7 +4833,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; @@ -4885,8 +4846,8 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { 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); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); @@ -4984,7 +4945,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; @@ -4997,8 +4958,8 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { 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); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); @@ -5103,7 +5064,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; @@ -5118,8 +5079,8 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { 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); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); let format = editor.update(cx, |editor, cx| { @@ -5879,8 +5840,9 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { 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 buffer = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); + let multibuffer = cx.build_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( buffer.clone(), @@ -5900,7 +5862,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { multibuffer }); - let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + let view = cx.add_window(|cx| build_editor(multibuffer, cx)); view.update(cx, |view, cx| { assert_eq!(view.text(cx), "aaaa\nbbbb"); view.change_selections(None, cx, |s| { @@ -5963,14 +5925,14 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { 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 buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text)); + let multibuffer = cx.build_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts(buffer, excerpt_ranges, cx); multibuffer }); - let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + let view = cx.add_window(|cx| build_editor(multibuffer, cx)); view.update(cx, |view, cx| { let (expected_text, selection_ranges) = marked_text_ranges( indoc! {" @@ -6021,9 +5983,10 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { 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 buffer = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); let mut excerpt1_id = None; - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.build_model(|cx| { let mut multibuffer = MultiBuffer::new(0); excerpt1_id = multibuffer .push_excerpts( @@ -6046,24 +6009,22 @@ fn test_refresh_selections(cx: &mut TestAppContext) { 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); + 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 + }); // Refreshing selections is a no-op when excerpts haven't changed. editor.update(cx, |editor, cx| { @@ -6108,9 +6069,10 @@ fn test_refresh_selections(cx: &mut TestAppContext) { 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 buffer = + cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); let mut excerpt1_id = None; - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.build_model(|cx| { let mut multibuffer = MultiBuffer::new(0); excerpt1_id = multibuffer .push_excerpts( @@ -6133,18 +6095,16 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { 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); + 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 + }); multibuffer.update(cx, |multibuffer, cx| { multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); @@ -6205,10 +6165,11 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { "{{} }\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); + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -6244,12 +6205,10 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { fn test_highlighted_ranges(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - build_editor(buffer.clone(), cx) - }) - .root(cx); + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + build_editor(buffer.clone(), cx) + }); editor.update(cx, |editor, cx| { struct Type1; @@ -6288,7 +6247,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { 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. + // highlight's `TypeId` which is non-executor. highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); assert_eq!( highlighted_ranges, @@ -6329,29 +6288,28 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { async fn test_following(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let buffer = project.update(cx, |project, cx| { let buffer = project .create_buffer(&sample_text(16, 8, 'a'), None, cx) .unwrap(); - cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) + cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)) + }); + let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx)); + let follower = cx.update(|cx| { + cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::from_points( + Point::new(0., 0.), + Point::new(10., 80.), + )), + ..Default::default() + }, + |cx| build_editor(buffer.clone(), cx), + ) }); - let leader = cx - .add_window(|cx| build_editor(buffer.clone(), cx)) - .root(cx); - let follower = cx - .update(|cx| { - cx.add_window( - WindowOptions { - bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), - ..Default::default() - }, - |cx| build_editor(buffer.clone(), cx), - ) - }) - .root(cx); let is_still_following = Rc::new(RefCell::new(true)); let follower_edit_event_count = Rc::new(RefCell::new(0)); @@ -6369,9 +6327,10 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .detach(); cx.subscribe(&follower, move |_, _, event, cx| { - if Editor::should_unfollow_on_event(event, cx) { + if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { *is_still_following.borrow_mut() = false; } + if let Event::BufferEdited = event { *follower_edit_event_count.borrow_mut() += 1; } @@ -6398,7 +6357,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { // Update the scroll position only leader.update(cx, |leader, cx| { - leader.set_scroll_position(vec2f(1.5, 3.5), cx); + leader.set_scroll_position(Point::new(1.5, 3.5), cx); }); follower .update(cx, |follower, cx| { @@ -6408,7 +6367,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .unwrap(); assert_eq!( follower.update(cx, |follower, cx| follower.scroll_position(cx)), - vec2f(1.5, 3.5) + Point::new(1.5, 3.5) ); assert_eq!(*is_still_following.borrow(), true); assert_eq!(*follower_edit_event_count.borrow(), 0); @@ -6418,7 +6377,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { leader.update(cx, |leader, cx| { leader.change_selections(None, cx, |s| s.select_ranges([0..0])); leader.request_autoscroll(Autoscroll::newest(), cx); - leader.set_scroll_position(vec2f(1.5, 3.5), cx); + leader.set_scroll_position(Point::new(1.5, 3.5), cx); }); follower .update(cx, |follower, cx| { @@ -6427,7 +6386,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .await .unwrap(); follower.update(cx, |follower, cx| { - assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0)); + assert_eq!(follower.scroll_position(cx), Point::new(1.5, 0.0)); assert_eq!(follower.selections.ranges(cx), vec![0..0]); }); assert_eq!(*is_still_following.borrow(), true); @@ -6468,7 +6427,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { follower.set_scroll_anchor( ScrollAnchor { anchor: top_anchor, - offset: vec2f(0.0, 0.5), + offset: Point::new(0.0, 0.5), }, cx, ); @@ -6480,15 +6439,13 @@ async fn test_following(cx: &mut gpui::TestAppContext) { async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), 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)); + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); cx.add_view(|cx| build_editor(multibuffer.clone(), cx)) }); @@ -6678,7 +6635,7 @@ fn test_combine_syntax_and_fuzzy_match_highlights() { 4..5, HighlightStyle { color: Some(Hsla::green()), - weight: Some(fonts::Weight::BOLD), + font_weight: Some(gpui::FontWeight::BOLD), ..Default::default() }, ), @@ -6693,14 +6650,14 @@ fn test_combine_syntax_and_fuzzy_match_highlights() { 6..8, HighlightStyle { color: Some(Hsla::green()), - weight: Some(fonts::Weight::BOLD), + font_weight: Some(gpui::FontWeight::BOLD), ..Default::default() }, ), ( 8..9, HighlightStyle { - weight: Some(fonts::Weight::BOLD), + font_weight: Some(gpui::FontWeight::BOLD), ..Default::default() }, ), @@ -6710,7 +6667,7 @@ fn test_combine_syntax_and_fuzzy_match_highlights() { #[gpui::test] async fn go_to_prev_overlapping_diagnostic( - deterministic: Arc, + executor: BackgroundExecutor, cx: &mut gpui::TestAppContext, ) { init_test(cx, |_| {}); @@ -6765,7 +6722,7 @@ async fn go_to_prev_overlapping_diagnostic( }); }); - deterministic.run_until_parked(); + executor.run_until_parked(); cx.update_editor(|editor, cx| { editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); @@ -6805,7 +6762,7 @@ async fn go_to_prev_overlapping_diagnostic( } #[gpui::test] -async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppContext) { +async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; @@ -6840,7 +6797,7 @@ async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppCon ); cx.set_diff_base(Some(&diff_base)); - deterministic.run_until_parked(); + executor.run_until_parked(); cx.update_editor(|editor, cx| { //Wrap around the bottom of the buffer @@ -7012,7 +6969,7 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { } #[gpui::test(iterations = 10)] -async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppContext) { +async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let (copilot, copilot_lsp) = Copilot::fake(cx); @@ -7054,7 +7011,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC }], vec![], ); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.context_menu_visible()); assert!(!editor.has_active_copilot_suggestion(cx)); @@ -7096,7 +7053,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC }], vec![], ); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_copilot_suggestion(cx)); @@ -7129,7 +7086,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC }], vec![], ); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.context_menu_visible()); assert!(!editor.has_active_copilot_suggestion(cx)); @@ -7144,7 +7101,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC // Ensure existing completion is interpolated when inserting again. cx.simulate_keystroke("c"); - deterministic.run_until_parked(); + executor.run_until_parked(); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_copilot_suggestion(cx)); @@ -7162,7 +7119,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC }], vec![], ); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_copilot_suggestion(cx)); @@ -7241,7 +7198,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC ); cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.has_active_copilot_suggestion(cx)); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); @@ -7263,7 +7220,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC #[gpui::test] async fn test_copilot_completion_invalidation( - deterministic: Arc, + executor: BackgroundExecutor, cx: &mut gpui::TestAppContext, ) { init_test(cx, |_| {}); @@ -7298,7 +7255,7 @@ async fn test_copilot_completion_invalidation( vec![], ); cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.has_active_copilot_suggestion(cx)); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); @@ -7329,18 +7286,15 @@ async fn test_copilot_completion_invalidation( } #[gpui::test] -async fn test_copilot_multibuffer( - deterministic: Arc, - cx: &mut gpui::TestAppContext, -) { +async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let (copilot, copilot_lsp) = Copilot::fake(cx); cx.update(|cx| cx.set_global(copilot)); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); - let multibuffer = cx.add_model(|cx| { + let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n")); + let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n")); + let multibuffer = cx.build_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( buffer_1.clone(), @@ -7360,7 +7314,7 @@ async fn test_copilot_multibuffer( ); multibuffer }); - let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)); handle_copilot_completion_request( &copilot_lsp, @@ -7378,7 +7332,7 @@ async fn test_copilot_multibuffer( }); editor.next_copilot_suggestion(&Default::default(), cx); }); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); editor.update(cx, |editor, cx| { assert!(editor.has_active_copilot_suggestion(cx)); assert_eq!( @@ -7420,7 +7374,7 @@ async fn test_copilot_multibuffer( }); // Ensure the new suggestion is displayed when the debounce timeout expires. - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); editor.update(cx, |editor, cx| { assert!(editor.has_active_copilot_suggestion(cx)); assert_eq!( @@ -7432,10 +7386,7 @@ async fn test_copilot_multibuffer( } #[gpui::test] -async fn test_copilot_disabled_globs( - deterministic: Arc, - cx: &mut gpui::TestAppContext, -) { +async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings .copilot @@ -7446,7 +7397,7 @@ async fn test_copilot_disabled_globs( let (copilot, copilot_lsp) = Copilot::fake(cx); cx.update(|cx| cx.set_global(copilot)); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/test", json!({ @@ -7470,7 +7421,7 @@ async fn test_copilot_disabled_globs( .await .unwrap(); - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.build_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( private_buffer.clone(), @@ -7490,7 +7441,7 @@ async fn test_copilot_disabled_globs( ); multibuffer }); - let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)); let mut copilot_requests = copilot_lsp .handle_request::(move |_params, _cx| async move { @@ -7510,7 +7461,7 @@ async fn test_copilot_disabled_globs( editor.next_copilot_suggestion(&Default::default(), cx); }); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); assert!(copilot_requests.try_next().is_err()); editor.update(cx, |editor, cx| { @@ -7520,7 +7471,7 @@ async fn test_copilot_disabled_globs( editor.next_copilot_suggestion(&Default::default(), cx); }); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); assert!(copilot_requests.try_next().is_ok()); } @@ -7558,7 +7509,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/a", json!({ @@ -7569,9 +7520,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { .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 workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -7665,7 +7614,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/a", json!({ @@ -7982,7 +7931,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; @@ -7996,8 +7945,8 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { .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); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)); editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); let format = editor.update(cx, |editor, cx| { @@ -8166,11 +8115,9 @@ pub(crate) fn update_test_project_settings( } 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); + theme::init(cx); client::init_settings(cx); language::init(cx); Project::init_settings(cx); diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index 2d3be45a00..ec37c57f2c 100644 --- a/crates/editor2/src/test.rs +++ b/crates/editor2/src/test.rs @@ -60,6 +60,7 @@ pub fn assert_text_with_selections( #[allow(dead_code)] #[cfg(any(test, feature = "test-support"))] pub(crate) fn build_editor(buffer: Model, cx: &mut ViewContext) -> Editor { + // todo!() Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx) } @@ -68,5 +69,6 @@ pub(crate) fn build_editor_with_project( buffer: Model, cx: &mut ViewContext, ) -> Editor { + // todo!() 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 0ee6b33a2f..304e4fcd2b 100644 --- a/crates/editor2/src/test/editor_test_context.rs +++ b/crates/editor2/src/test/editor_test_context.rs @@ -27,7 +27,7 @@ pub struct EditorTestContext<'a> { impl<'a> EditorTestContext<'a> { pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); // fs.insert_file("/file", "".to_owned()).await; fs.insert_tree( "/root", diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index f196923ad8..cbee96f037 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1008,11 +1008,14 @@ impl Context for AppContext { read(entity, self) } - fn read_window( + fn read_window( &self, - window: &AnyWindowHandle, - read: impl FnOnce(AnyView, &AppContext) -> R, - ) -> Result { + window: &WindowHandle, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Result + where + T: 'static, + { let window = self .windows .get(window.id) @@ -1021,7 +1024,11 @@ impl Context for AppContext { .unwrap(); let root_view = window.root_view.clone().unwrap(); - Ok(read(root_view, self)) + let view = root_view + .downcast::() + .map_err(|_| anyhow!("root view's type has changed"))?; + + Ok(read(view.read(self), self)) } } diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index b9eeacb262..0a38d6c76a 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -67,11 +67,14 @@ impl Context for AsyncAppContext { lock.update_window(window, f) } - fn read_window( + fn read_window( &self, - window: &AnyWindowHandle, - read: impl FnOnce(AnyView, &AppContext) -> R, - ) -> Result { + window: &WindowHandle, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Result + where + T: 'static, + { let app = self.app.upgrade().context("app was released")?; let lock = app.borrow(); lock.read_window(window, read) @@ -261,11 +264,14 @@ impl Context for AsyncWindowContext { self.app.read_model(handle, read) } - fn read_window( + fn read_window( &self, - window: &AnyWindowHandle, - read: impl FnOnce(AnyView, &AppContext) -> R, - ) -> Result { + window: &WindowHandle, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Result + where + T: 'static, + { self.app.read_window(window, read) } } diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index 1fe9a09ba7..6bc5ba30dd 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -1,6 +1,6 @@ use crate::{ AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId, - EventEmitter, Model, Subscription, Task, WeakModel, WindowContext, + EventEmitter, Model, Subscription, Task, WeakModel, WindowContext, WindowHandle, }; use anyhow::Result; use derive_more::{Deref, DerefMut}; @@ -240,11 +240,14 @@ impl<'a, T> Context for ModelContext<'a, T> { self.app.read_model(handle, read) } - fn read_window( + fn read_window( &self, - window: &AnyWindowHandle, - read: impl FnOnce(AnyView, &AppContext) -> R, - ) -> Result { + window: &WindowHandle, + read: impl FnOnce(&U, &AppContext) -> R, + ) -> Result + where + U: 'static, + { self.app.read_window(window, read) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index f8d5c01160..64895307d2 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -59,11 +59,14 @@ impl Context for TestAppContext { app.read_model(handle, read) } - fn read_window( + fn read_window( &self, - window: &AnyWindowHandle, - read: impl FnOnce(AnyView, &AppContext) -> R, - ) -> Result { + window: &WindowHandle, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Result + where + T: 'static, + { let app = self.app.borrow(); app.read_window(window, read) } @@ -102,8 +105,8 @@ impl TestAppContext { Ok(()) } - pub fn executor(&self) -> &BackgroundExecutor { - &self.background_executor + pub fn executor(&self) -> BackgroundExecutor { + self.background_executor.clone() } pub fn foreground_executor(&self) -> &ForegroundExecutor { diff --git a/crates/gpui2/src/color.rs b/crates/gpui2/src/color.rs index 3ae247962b..c9d7bdc139 100644 --- a/crates/gpui2/src/color.rs +++ b/crates/gpui2/src/color.rs @@ -154,6 +154,30 @@ impl Hsla { pub fn to_rgb(self) -> Rgba { self.into() } + + pub fn red() -> Self { + red() + } + + pub fn green() -> Self { + green() + } + + pub fn blue() -> Self { + blue() + } + + pub fn black() -> Self { + black() + } + + pub fn white() -> Self { + white() + } + + pub fn transparent_black() -> Self { + transparent_black() + } } impl Eq for Hsla {} @@ -212,6 +236,15 @@ pub fn blue() -> Hsla { } } +pub fn green() -> Hsla { + Hsla { + h: 0.3, + s: 1., + l: 0.5, + a: 1., + } +} + impl Hsla { /// Returns true if the HSLA color is fully transparent, false otherwise. pub fn is_transparent(&self) -> bool { diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index ff150824b1..1419010bdd 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -105,11 +105,13 @@ pub trait Context { where F: FnOnce(AnyView, &mut WindowContext<'_>) -> T; - fn read_window( + fn read_window( &self, - window: &AnyWindowHandle, - read: impl FnOnce(AnyView, &AppContext) -> R, - ) -> Result; + window: &WindowHandle, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Result + where + T: 'static; } pub trait VisualContext: Context { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index a9d2d55453..3e49ffd49b 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -10,7 +10,7 @@ use crate::{ SharedString, Size, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; use derive_more::{Deref, DerefMut}; use futures::{ @@ -1429,16 +1429,25 @@ impl Context for WindowContext<'_> { read(&*entity, &*self.app) } - fn read_window( + fn read_window( &self, - window: &AnyWindowHandle, - read: impl FnOnce(AnyView, &AppContext) -> R, - ) -> Result { - if window == &self.window.handle { - let root_view = self.window.root_view.clone().unwrap(); - Ok(read(root_view, self)) + window: &WindowHandle, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + if window.any_handle == self.window.handle { + let root_view = self + .window + .root_view + .clone() + .unwrap() + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + Ok(read(root_view.read(self), self)) } else { - window.read(self.app, read) + self.app.read_window(window, read) } } } @@ -2257,11 +2266,14 @@ impl Context for ViewContext<'_, V> { self.window_cx.read_model(handle, read) } - fn read_window( + fn read_window( &self, - window: &AnyWindowHandle, - read: impl FnOnce(AnyView, &AppContext) -> R, - ) -> Result { + window: &WindowHandle, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Result + where + T: 'static, + { self.window_cx.read_window(window, read) } } @@ -2335,14 +2347,6 @@ impl WindowHandle { } } - pub fn root(&self, cx: &C) -> Result> { - cx.read_window(&self.any_handle, |root_view, _| { - root_view - .downcast::() - .map_err(|_| anyhow!("the type of the window's root view has changed")) - })? - } - pub fn update( self, cx: &mut C, @@ -2358,6 +2362,29 @@ impl WindowHandle { Ok(cx.update_view(&view, update)) })? } + + pub fn read<'a>(&self, cx: &'a AppContext) -> Result<&'a V> { + let x = cx + .windows + .get(self.id) + .and_then(|window| { + window + .as_ref() + .and_then(|window| window.root_view.clone()) + .map(|root_view| root_view.downcast::()) + }) + .ok_or_else(|| anyhow!("window not found"))? + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + + Ok(x.read(cx)) + } + + pub fn read_with(self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result + where + C: Context, + { + cx.read_window(&self, |root_view: &V, cx| read_with(root_view, cx)) + } } impl Copy for WindowHandle {} @@ -2424,11 +2451,16 @@ impl AnyWindowHandle { cx.update_window(self, update) } - pub fn read(self, cx: &C, read: impl FnOnce(AnyView, &AppContext) -> R) -> Result + pub fn read(self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result where C: Context, + T: 'static, { - cx.read_window(&self, read) + let view = self + .downcast::() + .context("the type of the window's root view has changed")?; + + cx.read_window(&view, read) } } diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index bdea440c08..311049f032 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -1858,7 +1858,7 @@ mod tests { async fn test_first_line_pattern(cx: &mut TestAppContext) { let mut languages = LanguageRegistry::test(); - languages.set_executor(cx.executor().clone()); + languages.set_executor(cx.executor()); let languages = Arc::new(languages); languages.register( "/javascript", @@ -1895,7 +1895,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_language_loading(cx: &mut TestAppContext) { let mut languages = LanguageRegistry::test(); - languages.set_executor(cx.executor().clone()); + languages.set_executor(cx.executor()); let languages = Arc::new(languages); languages.register( "/JSON", diff --git a/crates/prettier2/src/prettier2.rs b/crates/prettier2/src/prettier2.rs index 44151774ae..75cc614eb6 100644 --- a/crates/prettier2/src/prettier2.rs +++ b/crates/prettier2/src/prettier2.rs @@ -490,7 +490,7 @@ mod tests { #[gpui::test] async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -563,7 +563,7 @@ mod tests { #[gpui::test] async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -628,7 +628,7 @@ mod tests { #[gpui::test] async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -686,7 +686,7 @@ mod tests { #[gpui::test] async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -742,7 +742,7 @@ mod tests { async fn test_prettier_lookup_in_npm_workspaces_for_not_installed( cx: &mut gpui::TestAppContext, ) { - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 95f04dfc82..2cfe6b8d5d 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -863,7 +863,7 @@ impl Project { cx: &mut gpui::TestAppContext, ) -> Model { let mut languages = LanguageRegistry::test(); - languages.set_executor(cx.executor().clone()); + languages.set_executor(cx.executor()); let http_client = util::http::FakeHttpClient::with_404_response(); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx)); diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index 19485b2306..97b6ed9c74 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -89,7 +89,7 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) { async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/the-root", json!({ @@ -189,7 +189,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/the-root", json!({ @@ -547,7 +547,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/the-root", json!({ @@ -734,7 +734,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -826,7 +826,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -914,7 +914,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -1046,7 +1046,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "" })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; @@ -1125,7 +1125,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "x" })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; @@ -1215,7 +1215,7 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "" })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; @@ -1279,7 +1279,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) .await; @@ -1401,7 +1401,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { " .unindent(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": text })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; @@ -1671,7 +1671,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { "let three = 3;\n", ); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": text })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; @@ -1734,7 +1734,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "one two three" })) .await; @@ -1813,7 +1813,7 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { " .unindent(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -1959,7 +1959,7 @@ async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAp " .unindent(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2067,7 +2067,7 @@ async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) { " .unindent(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2187,7 +2187,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { ); let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2299,7 +2299,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2396,7 +2396,7 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2451,7 +2451,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2559,7 +2559,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { async fn test_save_file(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2591,7 +2591,7 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2622,7 +2622,7 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { async fn test_save_as(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({})).await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; @@ -2830,7 +2830,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2881,7 +2881,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -2927,7 +2927,7 @@ async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3074,7 +3074,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { init_test(cx); let initial_contents = "aaa\nbbbbb\nc\n"; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3154,7 +3154,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3216,7 +3216,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/the-dir", json!({ @@ -3479,7 +3479,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3596,7 +3596,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { async fn test_search(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3655,7 +3655,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { let search_query = "file"; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3767,7 +3767,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { let search_query = "file"; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ @@ -3878,7 +3878,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex let search_query = "file"; - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", json!({ diff --git a/crates/rpc2/src/peer.rs b/crates/rpc2/src/peer.rs index 80a2ab4378..20a36efdfe 100644 --- a/crates/rpc2/src/peer.rs +++ b/crates/rpc2/src/peer.rs @@ -577,18 +577,18 @@ mod tests { let client2 = Peer::new(0); let (client1_to_server_conn, server_to_client_1_conn, _kill) = - Connection::in_memory(cx.executor().clone()); + Connection::in_memory(cx.executor()); let (client1_conn_id, io_task1, client1_incoming) = - client1.add_test_connection(client1_to_server_conn, cx.executor().clone()); + client1.add_test_connection(client1_to_server_conn, cx.executor()); let (_, io_task2, server_incoming1) = - server.add_test_connection(server_to_client_1_conn, cx.executor().clone()); + server.add_test_connection(server_to_client_1_conn, cx.executor()); let (client2_to_server_conn, server_to_client_2_conn, _kill) = - Connection::in_memory(cx.executor().clone()); + Connection::in_memory(cx.executor()); let (client2_conn_id, io_task3, client2_incoming) = - client2.add_test_connection(client2_to_server_conn, cx.executor().clone()); + client2.add_test_connection(client2_to_server_conn, cx.executor()); let (_, io_task4, server_incoming2) = - server.add_test_connection(server_to_client_2_conn, cx.executor().clone()); + server.add_test_connection(server_to_client_2_conn, cx.executor()); executor.spawn(io_task1).detach(); executor.spawn(io_task2).detach(); @@ -763,16 +763,16 @@ mod tests { #[gpui::test(iterations = 50)] async fn test_dropping_request_before_completion(cx: &mut TestAppContext) { - let executor = cx.executor().clone(); + let executor = cx.executor(); let server = Peer::new(0); let client = Peer::new(0); let (client_to_server_conn, server_to_client_conn, _kill) = - Connection::in_memory(cx.executor().clone()); + Connection::in_memory(cx.executor()); let (client_to_server_conn_id, io_task1, mut client_incoming) = - client.add_test_connection(client_to_server_conn, cx.executor().clone()); + client.add_test_connection(client_to_server_conn, cx.executor()); let (server_to_client_conn_id, io_task2, mut server_incoming) = - server.add_test_connection(server_to_client_conn, cx.executor().clone()); + server.add_test_connection(server_to_client_conn, cx.executor()); executor.spawn(io_task1).detach(); executor.spawn(io_task2).detach(); diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 24ec810ac5..9514e9e492 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3443,27 +3443,27 @@ impl Workspace { }) } - // todo!() - // #[cfg(any(test, feature = "test-support"))] - // pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { - // use node_runtime::FakeNodeRuntime; + #[cfg(any(test, feature = "test-support"))] + pub fn test_new(project: Model, cx: &mut ViewContext) -> Self { + use gpui::Context; + use node_runtime::FakeNodeRuntime; - // let client = project.read(cx).client(); - // let user_store = project.read(cx).user_store(); + let client = project.read(cx).client(); + let user_store = project.read(cx).user_store(); - // let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); - // let app_state = Arc::new(AppState { - // languages: project.read(cx).languages().clone(), - // workspace_store, - // client, - // user_store, - // fs: project.read(cx).fs().clone(), - // build_window_options: |_, _, _| Default::default(), - // initialize_workspace: |_, _, _, _| Task::ready(Ok(())), - // node_runtime: FakeNodeRuntime::new(), - // }); - // Self::new(0, project, app_state, cx) - // } + let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); + let app_state = Arc::new(AppState { + languages: project.read(cx).languages().clone(), + workspace_store, + client, + user_store, + fs: project.read(cx).fs().clone(), + build_window_options: |_, _, _| Default::default(), + initialize_workspace: |_, _, _, _| Task::ready(Ok(())), + node_runtime: FakeNodeRuntime::new(), + }); + Self::new(0, project, app_state, cx) + } // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { // let dock = match position { From 656eb9d4551affb84935a65b7df60c182a2174b7 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 9 Nov 2023 00:18:00 -0800 Subject: [PATCH 006/126] WIP --- crates/editor2/src/editor_tests.rs | 550 ++++++++++-------- .../src/test/editor_lsp_test_context.rs | 1 + crates/gpui2/src/app.rs | 4 +- crates/gpui2/src/app/async_context.rs | 4 +- crates/gpui2/src/app/model_context.rs | 4 +- crates/gpui2/src/app/test_context.rs | 108 +++- crates/gpui2/src/color.rs | 2 +- crates/gpui2/src/gpui2.rs | 2 +- crates/gpui2/src/window.rs | 25 +- 9 files changed, 451 insertions(+), 249 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 08c44d7682..22eb6b3a08 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -10,8 +10,9 @@ use crate::{ use drag_and_drop::DragAndDrop; use futures::StreamExt; use gpui::{ + div, serde_json::{self, json}, - TestAppContext, WindowOptions, + Div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions, }; use indoc::indoc; use language::{ @@ -31,7 +32,7 @@ use util::{ test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, }; use workspace::{ - item::{FollowableEvents, FollowableItem, Item, ItemHandle}, + item::{FollowEvent, FollowableEvents, FollowableItem, Item, ItemHandle}, NavigationEntry, ViewId, }; @@ -470,16 +471,22 @@ fn test_clone(cx: &mut TestAppContext) { ); }); - let cloned_editor = editor.update(cx, |editor, cx| { - cx.add_window(Default::default(), |cx| editor.clone(cx)) - }); + let cloned_editor = editor + .update(cx, |editor, cx| { + cx.open_window(Default::default(), |cx| { + cx.build_view(|cx| editor.clone(cx)) + }) + }) + .unwrap(); - let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); - let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); + let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap(); + let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)).unwrap(); assert_eq!( - cloned_editor.update(cx, |e, cx| e.display_text(cx)), - editor.update(cx, |e, cx| e.display_text(cx)) + cloned_editor + .update(cx, |e, cx| e.display_text(cx)) + .unwrap(), + editor.update(cx, |e, cx| e.display_text(cx)).unwrap() ); assert_eq!( cloned_snapshot @@ -488,12 +495,20 @@ fn test_clone(cx: &mut TestAppContext) { 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)) + cloned_editor + .read_with(cx, |editor, cx| editor.selections.ranges::(cx)) + .unwrap(), + editor + .read_with(cx, |editor, cx| editor.selections.ranges(cx)) + .unwrap() ); assert_set_eq!( - cloned_editor.update(cx, |e, cx| e.selections.display_ranges(cx)), - editor.update(cx, |e, cx| e.selections.display_ranges(cx)) + cloned_editor + .update(cx, |e, cx| e.selections.display_ranges(cx)) + .unwrap(), + editor + .update(cx, |e, cx| e.selections.display_ranges(cx)) + .unwrap() ); } @@ -506,110 +521,113 @@ async fn test_navigation_history(cx: &mut TestAppContext) { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window; - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); + let pane = workspace + .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); - 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))); + workspace.update(cx, |v, cx| { + cx.build_view(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); + let mut editor = build_editor(buffer.clone(), cx); + let handle = cx.view(); + editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); - fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { - editor.nav_history.as_mut().unwrap().pop_backward(cx) - } + 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 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 large distance. + // The history can jump back to the previous position. + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)]) + }); + let nav_entry = pop_history(&mut editor, cx).unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item.id(), cx.entity_id()); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); - // Move the cursor a small distance via the mouse. - // Nothing is added to the navigation history. - editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); - editor.end_selection(cx); - assert_eq!( - editor.selections.display_ranges(cx), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] - ); - assert!(pop_history(&mut editor, cx).is_none()); + // Move the cursor a 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()); + // Move the cursor a large distance via the mouse. + // The history can jump back to the previous position. + editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] + ); + let nav_entry = pop_history(&mut editor, cx).unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item.id(), cx.entity_id()); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); - // Set scroll position to check later - editor.set_scroll_position(Point::::new(5.5, 5.5), cx); - let original_scroll_position = editor.scroll_manager.anchor(); + // Set scroll position to check later + editor.set_scroll_position(gpui::Point::::new(5.5, 5.5), cx); + let original_scroll_position = editor.scroll_manager.anchor(); - // Jump to the end of the document and adjust scroll - editor.move_to_end(&MoveToEnd, cx); - editor.set_scroll_position(Point::::new(-2.5, -0.5), cx); - assert_ne!(editor.scroll_manager.anchor(), original_scroll_position); + // Jump to the end of the document and adjust scroll + editor.move_to_end(&MoveToEnd, cx); + editor.set_scroll_position(gpui::Point::::new(-2.5, -0.5), cx); + assert_ne!(editor.scroll_manager.anchor(), original_scroll_position); - let nav_entry = pop_history(&mut editor, cx).unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); + 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), - Point::new(0., editor.max_point(cx).row() as f32) - ); + // Ensure we don't panic when navigation data contains invalid anchors *and* points. + let mut invalid_anchor = editor.scroll_manager.anchor().anchor; + invalid_anchor.text_anchor.buffer_id = Some(999); + let invalid_point = Point::new(9999, 0); + editor.navigate( + Box::new(NavigationData { + cursor_anchor: invalid_anchor, + cursor_position: invalid_point, + scroll_anchor: ScrollAnchor { + anchor: invalid_anchor, + offset: Default::default(), + }, + scroll_top_row: invalid_point.row, + }), + cx, + ); + assert_eq!( + editor.selections.display_ranges(cx), + &[editor.max_point(cx)..editor.max_point(cx)] + ); + assert_eq!( + editor.scroll_position(cx), + gpui::Point::new(0., editor.max_point(cx).row() as f32) + ); - editor + editor + }) }); } @@ -624,21 +642,11 @@ fn test_cancel(cx: &mut TestAppContext) { view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); - view.update_selection( - DisplayPoint::new(1, 1), - 0, - gpui::Point::::zero(), - cx, - ); + view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); view.end_selection(cx); view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); - view.update_selection( - DisplayPoint::new(0, 3), - 0, - gpui::Point::::zero(), - cx, - ); + view.update_selection(DisplayPoint::new(0, 3), 0, gpui::Point::::zero(), cx); view.end_selection(cx); assert_eq!( view.selections.display_ranges(cx), @@ -1223,7 +1231,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { }); view.update(cx, |view, cx| { - view.set_wrap_width(Some(140.), cx); + view.set_wrap_width(Some(140.0.into()), cx); assert_eq!( view.display_text(cx), "use one::{\n two::three::\n four::five\n};" @@ -1278,7 +1286,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); let window = cx.window; - window.simulate_resize(Point::new(100., 4. * line_height), &mut cx); + window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx); cx.set_state( &r#"ˇone @@ -1394,18 +1402,36 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { ); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 0.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 0.) + ); editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 3.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.) + ); editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 6.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 6.) + ); editor.scroll_screen(&ScrollAmount::Page(-1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 3.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.) + ); editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 1.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 1.) + ); editor.scroll_screen(&ScrollAmount::Page(0.5), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 3.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.) + ); }); } @@ -1420,7 +1446,7 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { }); let window = cx.window; - window.simulate_resize(Point::new(1000., 6.0 * line_height), &mut cx); + window.simulate_resize(gpui::Point::new(1000., 6.0 * line_height), &mut cx); cx.set_state( &r#"ˇone @@ -1436,7 +1462,10 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { "#, ); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 0.0)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 0.0) + ); }); // Add a cursor below the visible area. Since both cursors cannot fit @@ -1451,7 +1480,10 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { }) }); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 3.0)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.0) + ); }); // Move down. The editor cursor scrolls down to track the newest cursor. @@ -1459,7 +1491,10 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { editor.move_down(&Default::default(), cx); }); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 4.0)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 4.0) + ); }); // Add a cursor above the visible area. Since both cursors fit on screen, @@ -1473,7 +1508,10 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { }) }); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), Point::new(0., 1.0)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 1.0) + ); }); } @@ -1484,7 +1522,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); let window = cx.window; - window.simulate_resize(Point::new(100., 4. * line_height), &mut cx); + window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx); cx.set_state( &r#" @@ -3762,7 +3800,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| { view.selections.display_ranges(cx) }), + view.update(cx, |view, cx| { view.selections.display_ranges(cx) }) + .unwrap(), &[ DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), @@ -3774,7 +3813,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), &[ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), @@ -3785,7 +3825,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] ); @@ -3794,7 +3835,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] ); @@ -3802,7 +3844,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), &[ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), @@ -3813,7 +3856,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), &[ DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), @@ -3825,7 +3869,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), &[ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), @@ -3838,7 +3883,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), &[ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), @@ -3860,7 +3906,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), &[ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), @@ -4843,7 +4890,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { .await .unwrap(); - cx.foreground().start_waiting(); + cx.executor().start_waiting(); let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -4851,7 +4898,9 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { 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)); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); fake_server .handle_request::(move |params, _| async move { assert_eq!( @@ -4866,10 +4915,10 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { }) .next() .await; - cx.foreground().start_waiting(); + cx.executor().start_waiting(); save.await.unwrap(); assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), "one, two\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); @@ -4886,12 +4935,14 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { 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(); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); + cx.executor().advance_clock(super::FORMAT_TIMEOUT); + cx.executor().start_waiting(); save.await.unwrap(); assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), "one\ntwo\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); @@ -4907,7 +4958,9 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { ); }); - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); fake_server .handle_request::(move |params, _| async move { assert_eq!( @@ -4919,7 +4972,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { }) .next() .await; - cx.foreground().start_waiting(); + cx.executor().start_waiting(); save.await.unwrap(); } @@ -4955,7 +5008,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { .await .unwrap(); - cx.foreground().start_waiting(); + cx.executor().start_waiting(); let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -4963,7 +5016,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { 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)); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); fake_server .handle_request::(move |params, _| async move { assert_eq!( @@ -4978,10 +5033,10 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { }) .next() .await; - cx.foreground().start_waiting(); + cx.executor().start_waiting(); save.await.unwrap(); assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), "one, two\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); @@ -5000,12 +5055,14 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { unreachable!() }, ); - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); - cx.foreground().advance_clock(super::FORMAT_TIMEOUT); - cx.foreground().start_waiting(); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); + cx.executor().advance_clock(super::FORMAT_TIMEOUT); + cx.executor().start_waiting(); save.await.unwrap(); assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), "one\ntwo\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); @@ -5021,7 +5078,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { ); }); - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); fake_server .handle_request::(move |params, _| async move { assert_eq!( @@ -5033,7 +5092,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { }) .next() .await; - cx.foreground().start_waiting(); + cx.executor().start_waiting(); save.await.unwrap(); } @@ -5076,16 +5135,18 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { .await .unwrap(); - cx.foreground().start_waiting(); + cx.executor().start_waiting(); let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, 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) - }); + let format = editor + .update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }) + .unwrap(); fake_server .handle_request::(move |params, _| async move { assert_eq!( @@ -5100,10 +5161,10 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { }) .next() .await; - cx.foreground().start_waiting(); + cx.executor().start_waiting(); format.await.unwrap(); assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), "one, two\nthree\n" ); @@ -5117,14 +5178,16 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { 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(); + let format = editor + .update(cx, |editor, cx| { + editor.perform_format(project, FormatTrigger::Manual, cx) + }) + .unwrap(); + cx.executor().advance_clock(super::FORMAT_TIMEOUT); + cx.executor().start_waiting(); format.await.unwrap(); assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), "one\ntwo\nthree\n" ); } @@ -5150,7 +5213,7 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { // a newline and an indent before the `.` cx.lsp .handle_request::(move |_, cx| { - let executor = cx.background(); + let executor = cx.background_executor(); async move { executor.timer(Duration::from_millis(100)).await; Ok(Some(vec![lsp::TextEdit { @@ -5164,19 +5227,19 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { let format_1 = cx .update_editor(|editor, cx| editor.format(&Format, cx)) .unwrap(); - cx.foreground().run_until_parked(); + cx.executor().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(); + cx.executor().run_until_parked(); // Wait for both format requests to complete - cx.foreground().advance_clock(Duration::from_millis(200)); - cx.foreground().start_waiting(); + cx.executor().advance_clock(Duration::from_millis(200)); + cx.executor().start_waiting(); format_1.await.unwrap(); - cx.foreground().start_waiting(); + cx.executor().start_waiting(); format_2.await.unwrap(); // The formatting edits only happens once. @@ -5824,7 +5887,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { "# .unindent(), ); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); cx.assert_editor_state( &r#" @@ -6299,15 +6362,15 @@ async fn test_following(cx: &mut gpui::TestAppContext) { }); let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx)); let follower = cx.update(|cx| { - cx.add_window( + cx.open_window( WindowOptions { - bounds: WindowBounds::Fixed(RectF::from_points( - Point::new(0., 0.), - Point::new(10., 80.), + bounds: WindowBounds::Fixed(Bounds::from_corners( + gpui::Point::new((0. as f64).into(), (0. as f64).into()), + gpui::Point::new((10. as f64).into(), (80. as f64).into()), )), ..Default::default() }, - |cx| build_editor(buffer.clone(), cx), + |cx| cx.build_view(|cx| build_editor(buffer.clone(), cx)), ) }); @@ -6319,22 +6382,28 @@ async fn test_following(cx: &mut gpui::TestAppContext) { let is_still_following = is_still_following.clone(); let follower_edit_event_count = follower_edit_event_count.clone(); |_, cx| { - cx.subscribe(&leader, move |_, leader, event, cx| { - leader - .read(cx) - .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); - }) + cx.subscribe( + &leader.root_view(cx).unwrap(), + move |_, leader, event, cx| { + leader + .read(cx) + .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + }, + ) .detach(); - cx.subscribe(&follower, move |_, _, event, cx| { - if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { - *is_still_following.borrow_mut() = false; - } + cx.subscribe( + &follower.root_view(cx).unwrap(), + move |_, _, event: &Event, cx| { + if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { + *is_still_following.borrow_mut() = false; + } - if let Event::BufferEdited = event { - *follower_edit_event_count.borrow_mut() += 1; - } - }) + if let Event::BufferEdited = event { + *follower_edit_event_count.borrow_mut() += 1; + } + }, + ) .detach(); } }); @@ -6347,6 +6416,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .update(cx, |follower, cx| { follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) }) + .unwrap() .await .unwrap(); follower.read_with(cx, |follower, cx| { @@ -6357,17 +6427,20 @@ async fn test_following(cx: &mut gpui::TestAppContext) { // Update the scroll position only leader.update(cx, |leader, cx| { - leader.set_scroll_position(Point::new(1.5, 3.5), cx); + leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); }); follower .update(cx, |follower, cx| { follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) }) + .unwrap() .await .unwrap(); assert_eq!( - follower.update(cx, |follower, cx| follower.scroll_position(cx)), - Point::new(1.5, 3.5) + follower + .update(cx, |follower, cx| follower.scroll_position(cx)) + .unwrap(), + gpui::Point::new(1.5, 3.5) ); assert_eq!(*is_still_following.borrow(), true); assert_eq!(*follower_edit_event_count.borrow(), 0); @@ -6377,16 +6450,17 @@ async fn test_following(cx: &mut gpui::TestAppContext) { 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(Point::new(1.5, 3.5), cx); + leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); }); follower .update(cx, |follower, cx| { follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) }) + .unwrap() .await .unwrap(); follower.update(cx, |follower, cx| { - assert_eq!(follower.scroll_position(cx), Point::new(1.5, 0.0)); + assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0)); assert_eq!(follower.selections.ranges(cx), vec![0..0]); }); assert_eq!(*is_still_following.borrow(), true); @@ -6400,6 +6474,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .update(cx, |follower, cx| { follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) }) + .unwrap() .await .unwrap(); follower.read_with(cx, |follower, cx| { @@ -6415,6 +6490,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .update(cx, |follower, cx| { follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) }) + .unwrap() .await .unwrap(); follower.read_with(cx, |follower, cx| { @@ -6427,7 +6503,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { follower.set_scroll_anchor( ScrollAnchor { anchor: top_anchor, - offset: Point::new(0.0, 0.5), + offset: gpui::Point::new(0.0, 0.5), }, cx, ); @@ -6442,11 +6518,15 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + let pane = workspace + .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); + + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); let leader = pane.update(cx, |_, cx| { let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); - cx.add_view(|cx| build_editor(multibuffer.clone(), cx)) + cx.build_view(|cx| build_editor(multibuffer.clone(), cx)) }); // Start following the editor when it has no excerpts. @@ -6455,7 +6535,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .update(|cx| { Editor::from_state_proto( pane.clone(), - workspace.clone(), + workspace.root_view(cx).unwrap(), ViewId { creator: Default::default(), id: 0, @@ -6539,8 +6619,8 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .await .unwrap(); assert_eq!( - follower_1.read_with(cx, |editor, cx| editor.text(cx)), - leader.read_with(cx, |editor, cx| editor.text(cx)) + follower_1.update(cx, |editor, cx| editor.text(cx)), + leader.update(cx, |editor, cx| editor.text(cx)) ); update_message.borrow_mut().take(); @@ -6563,8 +6643,8 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .await .unwrap(); assert_eq!( - follower_2.read_with(cx, |editor, cx| editor.text(cx)), - leader.read_with(cx, |editor, cx| editor.text(cx)) + follower_2.update(cx, |editor, cx| editor.text(cx)), + leader.update(cx, |editor, cx| editor.text(cx)) ); // Remove some excerpts. @@ -6591,8 +6671,8 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .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)) + follower_1.update(cx, |editor, cx| editor.text(cx)), + leader.update(cx, |editor, cx| editor.text(cx)) ); } @@ -7521,11 +7601,16 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { let project = Project::test(fs, ["/a".as_ref()], cx).await; project.update(cx, |project, _| project.languages().add(Arc::new(language))); let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let worktree_id = workspace.update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() + + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }) }) - }); + .unwrap(); let buffer = project .update(cx, |project, cx| { @@ -7533,13 +7618,14 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { }) .await .unwrap(); - cx.foreground().run_until_parked(); - cx.foreground().start_waiting(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); let fake_server = fake_servers.next().await.unwrap(); let editor_handle = workspace .update(cx, |workspace, cx| { workspace.open_path((worktree_id, "main.rs"), None, true, cx) }) + .unwrap() .await .unwrap() .downcast::() @@ -7569,7 +7655,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { editor.handle_input("{", cx); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); buffer.read_with(cx, |buffer, _| { assert_eq!( @@ -7642,7 +7728,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 0, @@ -7659,7 +7745,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 0, @@ -7676,7 +7762,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 1, @@ -7693,7 +7779,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 1, @@ -7708,7 +7794,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 2, @@ -7865,7 +7951,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: // Trigger completion when typing a dash, because the dash is an extra // word character in the 'element' scope, which contains the cursor. cx.simulate_keystroke("-"); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); cx.update_editor(|editor, _| { if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( @@ -7878,7 +7964,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: }); cx.simulate_keystroke("l"); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); cx.update_editor(|editor, _| { if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( @@ -7894,7 +7980,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: // be the start of a subword. cx.set_state(r#"

"#); cx.simulate_keystroke("l"); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); cx.update_editor(|editor, _| { if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( @@ -7949,9 +8035,11 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { let editor = cx.add_window(|cx| build_editor(buffer, 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) - }); + let format = editor + .update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }) + .unwrap(); format.await.unwrap(); assert_eq!( editor.read_with(cx, |editor, cx| editor.text(cx)), diff --git a/crates/editor2/src/test/editor_lsp_test_context.rs b/crates/editor2/src/test/editor_lsp_test_context.rs index 59fa420d48..d48e911a9f 100644 --- a/crates/editor2/src/test/editor_lsp_test_context.rs +++ b/crates/editor2/src/test/editor_lsp_test_context.rs @@ -5,6 +5,7 @@ use std::{ }; use anyhow::Result; +use serde_json::json; use crate::{Editor, ToPoint}; use collections::HashSet; diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index cbee96f037..a56a8c24d4 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1011,7 +1011,7 @@ impl Context for AppContext { fn read_window( &self, window: &WindowHandle, - read: impl FnOnce(&T, &AppContext) -> R, + read: impl FnOnce(View, &AppContext) -> R, ) -> Result where T: 'static, @@ -1028,7 +1028,7 @@ impl Context for AppContext { .downcast::() .map_err(|_| anyhow!("root view's type has changed"))?; - Ok(read(view.read(self), self)) + Ok(read(view, self)) } } diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 0a38d6c76a..24510c18da 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -70,7 +70,7 @@ impl Context for AsyncAppContext { fn read_window( &self, window: &WindowHandle, - read: impl FnOnce(&T, &AppContext) -> R, + read: impl FnOnce(View, &AppContext) -> R, ) -> Result where T: 'static, @@ -267,7 +267,7 @@ impl Context for AsyncWindowContext { fn read_window( &self, window: &WindowHandle, - read: impl FnOnce(&T, &AppContext) -> R, + read: impl FnOnce(View, &AppContext) -> R, ) -> Result where T: 'static, diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index 6bc5ba30dd..d04f0f2289 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -1,6 +1,6 @@ use crate::{ AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId, - EventEmitter, Model, Subscription, Task, WeakModel, WindowContext, WindowHandle, + EventEmitter, Model, Subscription, Task, View, WeakModel, WindowContext, WindowHandle, }; use anyhow::Result; use derive_more::{Deref, DerefMut}; @@ -243,7 +243,7 @@ impl<'a, T> Context for ModelContext<'a, T> { fn read_window( &self, window: &WindowHandle, - read: impl FnOnce(&U, &AppContext) -> R, + read: impl FnOnce(View, &AppContext) -> R, ) -> Result where U: 'static, diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 64895307d2..82c7f11e1c 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,8 @@ use crate::{ AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, - Render, Result, Task, TestDispatcher, TestPlatform, ViewContext, VisualContext, WindowContext, - WindowHandle, WindowOptions, + Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext, VisualContext, + WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -62,7 +62,7 @@ impl Context for TestAppContext { fn read_window( &self, window: &WindowHandle, - read: impl FnOnce(&T, &AppContext) -> R, + read: impl FnOnce(View, &AppContext) -> R, ) -> Result where T: 'static, @@ -276,3 +276,105 @@ impl Model { .expect("model was dropped") } } + +use derive_more::{Deref, DerefMut}; +#[derive(Deref, DerefMut)] +pub struct VisualTestContext<'a> { + #[deref] + #[deref_mut] + cx: &'a mut TestAppContext, + window: AnyWindowHandle, +} + +impl<'a> VisualTestContext<'a> { + pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { + Self { cx, window } + } +} + +impl<'a> Context for VisualTestContext<'a> { + type Result = ::Result; + + fn build_model( + &mut self, + build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, + ) -> Self::Result> { + self.cx.build_model(build_model) + } + + fn update_model( + &mut self, + handle: &Model, + update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, + ) -> Self::Result + where + T: 'static, + { + self.cx.update_model(handle, update) + } + + fn read_model( + &self, + handle: &Model, + read: impl FnOnce(&T, &AppContext) -> R, + ) -> Self::Result + where + T: 'static, + { + self.cx.read_model(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, + { + self.cx.update_window(window, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(View, &AppContext) -> R, + ) -> Result + where + T: 'static, + { + self.cx.read_window(window, read) + } +} + +impl<'a> VisualContext for VisualTestContext<'a> { + fn build_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: 'static + Render, + { + self.window + .update(self.cx, |_, cx| cx.build_view(build_view)) + .unwrap() + } + + fn update_view( + &mut self, + view: &View, + update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, + ) -> Self::Result { + self.window + .update(self.cx, |_, cx| cx.update_view(view, update)) + .unwrap() + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> Self::Result> + where + V: Render, + { + self.window + .update(self.cx, |_, cx| cx.replace_root_view(build_view)) + .unwrap() + } +} diff --git a/crates/gpui2/src/color.rs b/crates/gpui2/src/color.rs index c9d7bdc139..3f751ae45a 100644 --- a/crates/gpui2/src/color.rs +++ b/crates/gpui2/src/color.rs @@ -141,7 +141,7 @@ impl TryFrom<&'_ str> for Rgba { } } -#[derive(Default, Copy, Clone, Debug, PartialEq)] +#[derive(Default, Copy, Clone, Debug, PartialEq, PartialOrd)] #[repr(C)] pub struct Hsla { pub h: f32, diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 1419010bdd..ef6c4a9e15 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -108,7 +108,7 @@ pub trait Context { fn read_window( &self, window: &WindowHandle, - read: impl FnOnce(&T, &AppContext) -> R, + read: impl FnOnce(View, &AppContext) -> R, ) -> Result where T: 'static; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 3e49ffd49b..0bd9a8bafd 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1432,7 +1432,7 @@ impl Context for WindowContext<'_> { fn read_window( &self, window: &WindowHandle, - read: impl FnOnce(&T, &AppContext) -> R, + read: impl FnOnce(View, &AppContext) -> R, ) -> Result where T: 'static, @@ -1445,7 +1445,7 @@ impl Context for WindowContext<'_> { .unwrap() .downcast::() .map_err(|_| anyhow!("the type of the window's root view has changed"))?; - Ok(read(root_view.read(self), self)) + Ok(read(root_view, self)) } else { self.app.read_window(window, read) } @@ -1769,6 +1769,10 @@ impl<'a, V: 'static> ViewContext<'a, V> { } } + pub fn entity_id(&self) -> EntityId { + self.view.entity_id() + } + pub fn view(&self) -> &View { self.view } @@ -2269,7 +2273,7 @@ impl Context for ViewContext<'_, V> { fn read_window( &self, window: &WindowHandle, - read: impl FnOnce(&T, &AppContext) -> R, + read: impl FnOnce(View, &AppContext) -> R, ) -> Result where T: 'static, @@ -2348,7 +2352,7 @@ impl WindowHandle { } pub fn update( - self, + &self, cx: &mut C, update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, ) -> Result @@ -2379,11 +2383,18 @@ impl WindowHandle { Ok(x.read(cx)) } - pub fn read_with(self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result + pub fn read_with(&self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result where C: Context, { - cx.read_window(&self, |root_view: &V, cx| read_with(root_view, cx)) + cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx)) + } + + pub fn root_view(&self, cx: &C) -> Result> + where + C: Context, + { + cx.read_window(self, |root_view, _cx| root_view.clone()) } } @@ -2451,7 +2462,7 @@ impl AnyWindowHandle { cx.update_window(self, update) } - pub fn read(self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result + pub fn read(self, cx: &C, read: impl FnOnce(View, &AppContext) -> R) -> Result where C: Context, T: 'static, From f5f9d881d757675cd25f663831bb84564a394c44 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 9 Nov 2023 11:57:13 -0700 Subject: [PATCH 007/126] Polish actions macros --- crates/gpui2/src/action.rs | 3 +-- crates/gpui2_macros/src/action.rs | 20 ++++++++++++++------ crates/menu2/src/menu2.rs | 5 +++-- crates/zed2/src/main.rs | 2 +- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 85149f5d55..39beb87ad2 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -145,8 +145,7 @@ macro_rules! actions { () => {}; ( $name:ident ) => { - #[gpui::register_action] - #[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)] + #[gpui::action] pub struct $name; }; diff --git a/crates/gpui2_macros/src/action.rs b/crates/gpui2_macros/src/action.rs index 66302f3fc0..564f35d6a4 100644 --- a/crates/gpui2_macros/src/action.rs +++ b/crates/gpui2_macros/src/action.rs @@ -34,13 +34,21 @@ pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream { let visibility = input.vis; let output = match input.data { - syn::Data::Struct(ref struct_data) => { - let fields = &struct_data.fields; - quote! { - #attributes - #visibility struct #name #fields + syn::Data::Struct(ref struct_data) => match &struct_data.fields { + syn::Fields::Named(_) | syn::Fields::Unnamed(_) => { + let fields = &struct_data.fields; + quote! { + #attributes + #visibility struct #name #fields + } } - } + syn::Fields::Unit => { + quote! { + #attributes + #visibility struct #name; + } + } + }, syn::Data::Enum(ref enum_data) => { let variants = &enum_data.variants; quote! { diff --git a/crates/menu2/src/menu2.rs b/crates/menu2/src/menu2.rs index 44ebffcca2..f761084937 100644 --- a/crates/menu2/src/menu2.rs +++ b/crates/menu2/src/menu2.rs @@ -1,9 +1,10 @@ use gpui::actions; -// todo!(remove this) +// If the zed binary doesn't use anything in this crate, it will be optimized out +// and the actions won't initialize. So we just call an empty initialization function. // https://github.com/rust-lang/rust/issues/47384 // https://github.com/mmastrac/rust-ctor/issues/280 -pub fn unused() {} +pub fn init() {} actions!( Cancel, diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index cd0f8e5fbf..52349edd2c 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -59,7 +59,7 @@ fn main() { //TODO!(figure out what the linker issues are here) // https://github.com/rust-lang/rust/issues/47384 // https://github.com/mmastrac/rust-ctor/issues/280 - menu::unused(); + // menu::unused(); let http = http::client(); init_paths(); init_logger(); From 408a495aaf7d7746453cc7621ee3c25b5f3aae82 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 9 Nov 2023 13:14:11 -0700 Subject: [PATCH 008/126] Call init --- crates/menu2/src/menu2.rs | 7 +++++-- crates/zed2/src/main.rs | 5 +---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/menu2/src/menu2.rs b/crates/menu2/src/menu2.rs index f761084937..6dfcce5d4f 100644 --- a/crates/menu2/src/menu2.rs +++ b/crates/menu2/src/menu2.rs @@ -1,7 +1,10 @@ use gpui::actions; -// If the zed binary doesn't use anything in this crate, it will be optimized out -// and the actions won't initialize. So we just call an empty initialization function. +// If the zed binary doesn't use anything in this crate, it will be optimized away +// and the actions won't initialize. So we just provide an empty initialization function +// to be called from main. +// +// These may provide relevant context: // https://github.com/rust-lang/rust/issues/47384 // https://github.com/mmastrac/rust-ctor/issues/280 pub fn init() {} diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 52349edd2c..309746181e 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -56,10 +56,7 @@ use zed2::{ mod open_listener; fn main() { - //TODO!(figure out what the linker issues are here) - // https://github.com/rust-lang/rust/issues/47384 - // https://github.com/mmastrac/rust-ctor/issues/280 - // menu::unused(); + menu::init(); let http = http::client(); init_paths(); init_logger(); From b9e098ead8ed508f02d2215876d1eb346e566877 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 9 Nov 2023 16:51:03 -0800 Subject: [PATCH 009/126] Start work on creating gpui2 version of project panel --- Cargo.lock | 31 + Cargo.toml | 1 + crates/project_panel2/Cargo.toml | 41 + .../project_panel2/src/file_associations.rs | 96 + crates/project_panel2/src/project_panel.rs | 2858 +++++++++++++++++ .../src/project_panel_settings.rs | 45 + crates/workspace2/src/workspace2.rs | 175 +- crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 2 +- crates/zed2/src/zed2.rs | 60 +- 10 files changed, 3140 insertions(+), 171 deletions(-) create mode 100644 crates/project_panel2/Cargo.toml create mode 100644 crates/project_panel2/src/file_associations.rs create mode 100644 crates/project_panel2/src/project_panel.rs create mode 100644 crates/project_panel2/src/project_panel_settings.rs diff --git a/Cargo.lock b/Cargo.lock index 8e2d8d7ace..01bfb08b0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6587,6 +6587,36 @@ dependencies = [ "workspace", ] +[[package]] +name = "project_panel2" +version = "0.1.0" +dependencies = [ + "anyhow", + "client2", + "collections", + "context_menu", + "db2", + "editor2", + "futures 0.3.28", + "gpui2", + "language2", + "menu2", + "postage", + "pretty_assertions", + "project2", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings2", + "smallvec", + "theme2", + "ui2", + "unicase", + "util", + "workspace2", +] + [[package]] name = "project_symbols" version = "0.1.0" @@ -11394,6 +11424,7 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project2", + "project_panel2", "rand 0.8.5", "regex", "rope2", diff --git a/Cargo.toml b/Cargo.toml index 1b8081d066..4dd2737e5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ members = [ "crates/project", "crates/project2", "crates/project_panel", + "crates/project_panel2", "crates/project_symbols", "crates/recent_projects", "crates/rope", diff --git a/crates/project_panel2/Cargo.toml b/crates/project_panel2/Cargo.toml new file mode 100644 index 0000000000..bd6bc59a65 --- /dev/null +++ b/crates/project_panel2/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "project_panel2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/project_panel.rs" +doctest = false + +[dependencies] +context_menu = { path = "../context_menu" } +collections = { path = "../collections" } +db = { path = "../db2", package = "db2" } +editor = { path = "../editor2", package = "editor2" } +gpui = { path = "../gpui2", package = "gpui2" } +menu = { path = "../menu2", package = "menu2" } +project = { path = "../project2", package = "project2" } +settings = { path = "../settings2", package = "settings2" } +theme = { path = "../theme2", package = "theme2" } +ui = { path = "../ui2", package = "ui2" } +util = { path = "../util" } +workspace = { path = "../workspace2", package = "workspace2" } +anyhow.workspace = true +postage.workspace = true +futures.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +schemars.workspace = true +smallvec.workspace = true +pretty_assertions.workspace = true +unicase = "2.6" + +[dev-dependencies] +client = { path = "../client2", package = "client2", features = ["test-support"] } +language = { path = "../language2", package = "language2", features = ["test-support"] } +editor = { path = "../editor2", package = "editor2", features = ["test-support"] } +gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] } +workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] } +serde_json.workspace = true diff --git a/crates/project_panel2/src/file_associations.rs b/crates/project_panel2/src/file_associations.rs new file mode 100644 index 0000000000..9e9a865f3e --- /dev/null +++ b/crates/project_panel2/src/file_associations.rs @@ -0,0 +1,96 @@ +use std::{path::Path, str, sync::Arc}; + +use collections::HashMap; + +use gpui::{AppContext, AssetSource}; +use serde_derive::Deserialize; +use util::{maybe, paths::PathExt}; + +#[derive(Deserialize, Debug)] +struct TypeConfig { + icon: Arc, +} + +#[derive(Deserialize, Debug)] +pub struct FileAssociations { + suffixes: HashMap, + types: HashMap, +} + +const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder"; +const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder"; +const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron"; +const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron"; +pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json"; + +pub fn init(assets: impl AssetSource, cx: &mut AppContext) { + cx.set_global(FileAssociations::new(assets)) +} + +impl FileAssociations { + pub fn new(assets: impl AssetSource) -> Self { + assets + .load("icons/file_icons/file_types.json") + .and_then(|file| { + serde_json::from_str::(str::from_utf8(&file).unwrap()) + .map_err(Into::into) + }) + .unwrap_or_else(|_| FileAssociations { + suffixes: HashMap::default(), + types: HashMap::default(), + }) + } + + pub fn get_icon(path: &Path, cx: &AppContext) -> Arc { + maybe!({ + let this = cx.has_global::().then(|| cx.global::())?; + + // FIXME: Associate a type with the languages and have the file's langauge + // override these associations + maybe!({ + let suffix = path.icon_suffix()?; + + this.suffixes + .get(suffix) + .and_then(|type_str| this.types.get(type_str)) + .map(|type_config| type_config.icon.clone()) + }) + .or_else(|| this.types.get("default").map(|config| config.icon.clone())) + }) + .unwrap_or_else(|| Arc::from("".to_string())) + } + + pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc { + maybe!({ + let this = cx.has_global::().then(|| cx.global::())?; + + let key = if expanded { + EXPANDED_DIRECTORY_TYPE + } else { + COLLAPSED_DIRECTORY_TYPE + }; + + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) + }) + .unwrap_or_else(|| Arc::from("".to_string())) + } + + pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc { + maybe!({ + let this = cx.has_global::().then(|| cx.global::())?; + + let key = if expanded { + EXPANDED_CHEVRON_TYPE + } else { + COLLAPSED_CHEVRON_TYPE + }; + + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) + }) + .unwrap_or_else(|| Arc::from("".to_string())) + } +} diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs new file mode 100644 index 0000000000..0844f5190c --- /dev/null +++ b/crates/project_panel2/src/project_panel.rs @@ -0,0 +1,2858 @@ +pub mod file_associations; +mod project_panel_settings; +use settings::Settings; + +use db::kvp::KEY_VALUE_STORE; +use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; +use file_associations::FileAssociations; + +use anyhow::{anyhow, Result}; +use gpui::{ + actions, div, px, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, + AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Model, + ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, + StatefulInteractivity, Styled, Task, UniformListScrollHandle, View, ViewContext, + VisualContext as _, WeakView, WindowContext, +}; +use menu::{Confirm, SelectNext, SelectPrev}; +use project::{ + repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, + Worktree, WorktreeId, +}; +use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use std::{ + cmp::Ordering, + collections::{hash_map, HashMap}, + ffi::OsStr, + ops::Range, + path::Path, + sync::Arc, +}; +use theme::ActiveTheme as _; +use ui::{h_stack, v_stack}; +use unicase::UniCase; +use util::TryFutureExt; +use workspace::{ + dock::{DockPosition, PanelEvent}, + Workspace, +}; + +const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; +const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; + +pub struct ProjectPanel { + project: Model, + fs: Arc, + list: UniformListScrollHandle, + focus_handle: FocusHandle, + visible_entries: Vec<(WorktreeId, Vec)>, + last_worktree_root_id: Option, + expanded_dir_ids: HashMap>, + selection: Option, + edit_state: Option, + filename_editor: View, + clipboard_entry: Option, + dragged_entry_destination: Option>, + workspace: WeakView, + has_focus: bool, + width: Option, + pending_serialization: Task>, +} + +#[derive(Copy, Clone, Debug)] +struct Selection { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, +} + +#[derive(Clone, Debug)] +struct EditState { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + is_new_entry: bool, + is_dir: bool, + processing_filename: Option, +} + +#[derive(Copy, Clone)] +pub enum ClipboardEntry { + Copied { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + }, + Cut { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + }, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct EntryDetails { + filename: String, + icon: Option>, + path: Arc, + depth: usize, + kind: EntryKind, + is_ignored: bool, + is_expanded: bool, + is_selected: bool, + is_editing: bool, + is_processing: bool, + is_cut: bool, + git_status: Option, +} + +actions!( + ExpandSelectedEntry, + CollapseSelectedEntry, + CollapseAllEntries, + NewDirectory, + NewFile, + Copy, + CopyPath, + CopyRelativePath, + RevealInFinder, + OpenInTerminal, + Cut, + Paste, + Delete, + Rename, + Open, + ToggleFocus, + NewSearchInDirectory, +); + +pub fn init_settings(cx: &mut AppContext) { + ProjectPanelSettings::register(cx); +} + +pub fn init(assets: impl AssetSource, cx: &mut AppContext) { + init_settings(cx); + file_associations::init(assets, cx); + // cx.add_action(ProjectPanel::expand_selected_entry); + // cx.add_action(ProjectPanel::collapse_selected_entry); + // cx.add_action(ProjectPanel::collapse_all_entries); + // cx.add_action(ProjectPanel::select_prev); + // cx.add_action(ProjectPanel::select_next); + // cx.add_action(ProjectPanel::new_file); + // cx.add_action(ProjectPanel::new_directory); + // cx.add_action(ProjectPanel::rename); + // cx.add_async_action(ProjectPanel::delete); + // cx.add_async_action(ProjectPanel::confirm); + // cx.add_async_action(ProjectPanel::open_file); + // cx.add_action(ProjectPanel::cancel); + // cx.add_action(ProjectPanel::cut); + // cx.add_action(ProjectPanel::copy); + // cx.add_action(ProjectPanel::copy_path); + // cx.add_action(ProjectPanel::copy_relative_path); + // cx.add_action(ProjectPanel::reveal_in_finder); + // cx.add_action(ProjectPanel::open_in_terminal); + // cx.add_action(ProjectPanel::new_search_in_directory); + // cx.add_action( + // |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { + // this.paste(action, cx); + // }, + // ); +} + +#[derive(Debug)] +pub enum Event { + OpenedEntry { + entry_id: ProjectEntryId, + focus_opened_item: bool, + }, + SplitEntry { + entry_id: ProjectEntryId, + }, + DockPositionChanged, + Focus, + NewSearchInDirectory { + dir_entry: Entry, + }, + ActivatePanel, +} + +#[derive(Serialize, Deserialize)] +struct SerializedProjectPanel { + width: Option, +} + +impl ProjectPanel { + fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { + let project = workspace.project().clone(); + let project_panel = cx.build_view(|cx: &mut ViewContext| { + cx.observe(&project, |this, _, cx| { + this.update_visible_entries(None, cx); + cx.notify(); + }) + .detach(); + let focus_handle = cx.focus_handle(); + + cx.on_focus(&focus_handle, Self::focus_in).detach(); + cx.on_blur(&focus_handle, Self::focus_out).detach(); + + cx.subscribe(&project, |this, project, event, cx| match event { + project::Event::ActiveEntryChanged(Some(entry_id)) => { + if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx) + { + this.expand_entry(worktree_id, *entry_id, cx); + this.update_visible_entries(Some((worktree_id, *entry_id)), cx); + this.autoscroll(cx); + cx.notify(); + } + } + project::Event::ActivateProjectPanel => { + cx.emit(Event::ActivatePanel); + } + project::Event::WorktreeRemoved(id) => { + this.expanded_dir_ids.remove(id); + this.update_visible_entries(None, cx); + cx.notify(); + } + _ => {} + }) + .detach(); + + let filename_editor = cx.build_view(|cx| Editor::single_line(cx)); + + cx.subscribe(&filename_editor, |this, _, event, cx| match event { + editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => { + this.autoscroll(cx); + } + _ => {} + }) + .detach(); + + // cx.observe_focus(&filename_editor, |this, _, is_focused, cx| { + // if !is_focused + // && this + // .edit_state + // .as_ref() + // .map_or(false, |state| state.processing_filename.is_none()) + // { + // this.edit_state = None; + // this.update_visible_entries(None, cx); + // } + // }) + // .detach(); + + // cx.observe_global::(|_, cx| { + // cx.notify(); + // }) + // .detach(); + + let view_id = cx.view().entity_id(); + let mut this = Self { + project: project.clone(), + fs: workspace.app_state().fs.clone(), + list: UniformListScrollHandle::new(), + focus_handle, + visible_entries: Default::default(), + last_worktree_root_id: Default::default(), + expanded_dir_ids: Default::default(), + selection: None, + edit_state: None, + filename_editor, + clipboard_entry: None, + // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), + dragged_entry_destination: None, + workspace: workspace.weak_handle(), + has_focus: false, + width: None, + pending_serialization: Task::ready(None), + }; + this.update_visible_entries(None, cx); + + // Update the dock position when the setting changes. + // todo!() + // let mut old_dock_position = this.position(cx); + // cx.observe_global::(move |this, cx| { + // let new_dock_position = this.position(cx); + // if new_dock_position != old_dock_position { + // old_dock_position = new_dock_position; + // cx.emit(Event::DockPositionChanged); + // } + // }) + // .detach(); + + this + }); + + cx.subscribe(&project_panel, { + let project_panel = project_panel.downgrade(); + move |workspace, _, event, cx| match event { + &Event::OpenedEntry { + entry_id, + focus_opened_item, + } => { + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { + if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + workspace + .open_path( + ProjectPath { + worktree_id: worktree.read(cx).id(), + path: entry.path.clone(), + }, + None, + focus_opened_item, + cx, + ) + .detach_and_log_err(cx); + if !focus_opened_item { + if let Some(project_panel) = project_panel.upgrade() { + let focus_handle = project_panel.read(cx).focus_handle.clone(); + cx.focus(&focus_handle); + } + } + } + } + } + &Event::SplitEntry { entry_id } => { + // if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { + // if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + // workspace + // .split_path( + // ProjectPath { + // worktree_id: worktree.read(cx).id(), + // path: entry.path.clone(), + // }, + // cx, + // ) + // .detach_and_log_err(cx); + // } + // } + } + _ => {} + } + }) + .detach(); + + project_panel + } + + pub fn load( + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + // let serialized_panel = if let Some(panel) = cx + // .background_executor() + // .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) }) + // .await + // .log_err() + // .flatten() + // { + // Some(serde_json::from_str::(&panel)?) + // } else { + // None + // }; + workspace.update(&mut cx, |workspace, cx| { + let panel = ProjectPanel::new(workspace, cx); + // if let Some(serialized_panel) = serialized_panel { + // panel.update(cx, |panel, cx| { + // panel.width = serialized_panel.width; + // cx.notify(); + // }); + // } + panel + }) + }) + } + + fn serialize(&mut self, cx: &mut ViewContext) { + let width = self.width; + self.pending_serialization = cx.background_executor().spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + PROJECT_PANEL_KEY.into(), + serde_json::to_string(&SerializedProjectPanel { width })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ); + } + + fn focus_in(&mut self, cx: &mut ViewContext) { + if !self.has_focus { + self.has_focus = true; + cx.emit(Event::Focus); + } + } + + fn focus_out(&mut self, _: &mut ViewContext) { + self.has_focus = false; + } + + fn deploy_context_menu( + &mut self, + position: Point, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) { + // let project = self.project.read(cx); + + // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { + // id + // } else { + // return; + // }; + + // self.selection = Some(Selection { + // worktree_id, + // entry_id, + // }); + + // let mut menu_entries = Vec::new(); + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let is_root = Some(entry) == worktree.root_entry(); + // if !project.is_remote() { + // menu_entries.push(ContextMenuItem::action( + // "Add Folder to Project", + // workspace::AddFolderToProject, + // )); + // if is_root { + // let project = self.project.clone(); + // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { + // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); + // })); + // } + // } + // menu_entries.push(ContextMenuItem::action("New File", NewFile)); + // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Cut", Cut)); + // menu_entries.push(ContextMenuItem::action("Copy", Copy)); + // if let Some(clipboard_entry) = self.clipboard_entry { + // if clipboard_entry.worktree_id() == worktree.id() { + // menu_entries.push(ContextMenuItem::action("Paste", Paste)); + // } + // } + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); + // menu_entries.push(ContextMenuItem::action( + // "Copy Relative Path", + // CopyRelativePath, + // )); + + // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::Separator); + // } + // menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); + // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); + // menu_entries.push(ContextMenuItem::action( + // "Search Inside", + // NewSearchInDirectory, + // )); + // } + + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Rename", Rename)); + // if !is_root { + // menu_entries.push(ContextMenuItem::action("Delete", Delete)); + // } + // } + + // // self.context_menu.update(cx, |menu, cx| { + // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); + // // }); + + // cx.notify(); + } + + fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + if entry.is_dir() { + let worktree_id = worktree.id(); + let entry_id = entry.id; + let expanded_dir_ids = + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + expanded_dir_ids + } else { + return; + }; + + match expanded_dir_ids.binary_search(&entry_id) { + Ok(_) => self.select_next(&SelectNext, cx), + Err(ix) => { + self.project.update(cx, |project, cx| { + project.expand_entry(worktree_id, entry_id, cx); + }); + + expanded_dir_ids.insert(ix, entry_id); + self.update_visible_entries(None, cx); + cx.notify(); + } + } + } + } + } + + fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext) { + if let Some((worktree, mut entry)) = self.selected_entry(cx) { + let worktree_id = worktree.id(); + let expanded_dir_ids = + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + expanded_dir_ids + } else { + return; + }; + + loop { + let entry_id = entry.id; + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.notify(); + break; + } + Err(_) => { + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } + } + } + } + } + } + + pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext) { + self.expanded_dir_ids.clear(); + self.update_visible_entries(None, cx); + cx.notify(); + } + + fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + self.project.update(cx, |project, cx| { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + } + Err(ix) => { + project.expand_entry(worktree_id, entry_id, cx); + expanded_dir_ids.insert(ix, entry_id); + } + } + }); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.focus(&self.focus_handle); + cx.notify(); + } + } + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + let (mut worktree_ix, mut entry_ix, _) = + self.index_for_selection(selection).unwrap_or_default(); + if entry_ix > 0 { + entry_ix -= 1; + } else if worktree_ix > 0 { + worktree_ix -= 1; + entry_ix = self.visible_entries[worktree_ix].1.len() - 1; + } else { + return; + } + + let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix]; + self.selection = Some(Selection { + worktree_id: *worktree_id, + entry_id: worktree_entries[entry_ix].id, + }); + self.autoscroll(cx); + cx.notify(); + } else { + self.select_first(cx); + } + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { + if let Some(task) = self.confirm_edit(cx) { + return Some(task); + } + + None + } + + fn open_file(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { + if let Some((_, entry)) = self.selected_entry(cx) { + if entry.is_file() { + self.open_entry(entry.id, true, cx); + } + } + + None + } + + fn confirm_edit(&mut self, cx: &mut ViewContext) -> Option>> { + let edit_state = self.edit_state.as_mut()?; + cx.focus(&self.focus_handle); + + let worktree_id = edit_state.worktree_id; + let is_new_entry = edit_state.is_new_entry; + let is_dir = edit_state.is_dir; + let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; + let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone(); + let filename = self.filename_editor.read(cx).text(cx); + + let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some(); + let edit_task; + let edited_entry_id; + if is_new_entry { + self.selection = Some(Selection { + worktree_id, + entry_id: NEW_ENTRY_ID, + }); + let new_path = entry.path.join(&filename.trim_start_matches("/")); + if path_already_exists(new_path.as_path()) { + return None; + } + + edited_entry_id = NEW_ENTRY_ID; + edit_task = self.project.update(cx, |project, cx| { + project.create_entry((worktree_id, &new_path), is_dir, cx) + })?; + } else { + let new_path = if let Some(parent) = entry.path.clone().parent() { + parent.join(&filename) + } else { + filename.clone().into() + }; + if path_already_exists(new_path.as_path()) { + return None; + } + + edited_entry_id = entry.id; + edit_task = self.project.update(cx, |project, cx| { + project.rename_entry(entry.id, new_path.as_path(), cx) + })?; + }; + + edit_state.processing_filename = Some(filename); + cx.notify(); + + Some(cx.spawn(|this, mut cx| async move { + let new_entry = edit_task.await; + this.update(&mut cx, |this, cx| { + this.edit_state.take(); + cx.notify(); + })?; + + let new_entry = new_entry?; + this.update(&mut cx, |this, cx| { + if let Some(selection) = &mut this.selection { + if selection.entry_id == edited_entry_id { + selection.worktree_id = worktree_id; + selection.entry_id = new_entry.id; + this.expand_to_selection(cx); + } + } + this.update_visible_entries(None, cx); + if is_new_entry && !is_dir { + this.open_entry(new_entry.id, true, cx); + } + cx.notify(); + })?; + Ok(()) + })) + } + + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + self.edit_state = None; + self.update_visible_entries(None, cx); + cx.focus(&self.focus_handle); + cx.notify(); + } + + fn open_entry( + &mut self, + entry_id: ProjectEntryId, + focus_opened_item: bool, + cx: &mut ViewContext, + ) { + cx.emit(Event::OpenedEntry { + entry_id, + focus_opened_item, + }); + } + + fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { + cx.emit(Event::SplitEntry { entry_id }); + } + + fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext) { + self.add_entry(false, cx) + } + + fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext) { + self.add_entry(true, cx) + } + + fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext) { + if let Some(Selection { + worktree_id, + entry_id, + }) = self.selection + { + let directory_id; + if let Some((worktree, expanded_dir_ids)) = self + .project + .read(cx) + .worktree_for_id(worktree_id, cx) + .zip(self.expanded_dir_ids.get_mut(&worktree_id)) + { + let worktree = worktree.read(cx); + if let Some(mut entry) = worktree.entry_for_id(entry_id) { + loop { + if entry.is_dir() { + if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(ix, entry.id); + } + directory_id = entry.id; + break; + } else { + if let Some(parent_path) = entry.path.parent() { + if let Some(parent_entry) = worktree.entry_for_path(parent_path) { + entry = parent_entry; + continue; + } + } + return; + } + } + } else { + return; + }; + } else { + return; + }; + + self.edit_state = Some(EditState { + worktree_id, + entry_id: directory_id, + is_new_entry: true, + is_dir, + processing_filename: None, + }); + self.filename_editor.update(cx, |editor, cx| { + editor.clear(cx); + editor.focus(cx); + }); + self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx); + self.autoscroll(cx); + cx.notify(); + } + } + + fn rename(&mut self, _: &Rename, cx: &mut ViewContext) { + if let Some(Selection { + worktree_id, + entry_id, + }) = self.selection + { + if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { + if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + self.edit_state = Some(EditState { + worktree_id, + entry_id, + is_new_entry: false, + is_dir: entry.is_dir(), + processing_filename: None, + }); + let file_name = entry + .path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_default() + .to_string(); + let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy()); + let selection_end = + file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); + self.filename_editor.update(cx, |editor, cx| { + editor.set_text(file_name, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([0..selection_end]) + }); + editor.focus(cx); + }); + self.update_visible_entries(None, cx); + self.autoscroll(cx); + cx.notify(); + } + } + + // cx.update_global(|drag_and_drop: &mut DragAndDrop, cx| { + // drag_and_drop.cancel_dragging::(cx); + // }) + } + } + + fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { + let Selection { entry_id, .. } = self.selection?; + let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; + let file_name = path.file_name()?; + + let mut answer = cx.prompt( + PromptLevel::Info, + &format!("Delete {file_name:?}?"), + &["Delete", "Cancel"], + ); + Some(cx.spawn(|this, mut cx| async move { + if answer.await != Ok(0) { + return Ok(()); + } + this.update(&mut cx, |this, cx| { + this.project + .update(cx, |project, cx| project.delete_entry(entry_id, cx)) + .ok_or_else(|| anyhow!("no such entry")) + })?? + .await + })) + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + let (mut worktree_ix, mut entry_ix, _) = + self.index_for_selection(selection).unwrap_or_default(); + if let Some((_, worktree_entries)) = self.visible_entries.get(worktree_ix) { + if entry_ix + 1 < worktree_entries.len() { + entry_ix += 1; + } else { + worktree_ix += 1; + entry_ix = 0; + } + } + + if let Some((worktree_id, worktree_entries)) = self.visible_entries.get(worktree_ix) { + if let Some(entry) = worktree_entries.get(entry_ix) { + self.selection = Some(Selection { + worktree_id: *worktree_id, + entry_id: entry.id, + }); + self.autoscroll(cx); + cx.notify(); + } + } + } else { + self.select_first(cx); + } + } + + fn select_first(&mut self, cx: &mut ViewContext) { + let worktree = self + .visible_entries + .first() + .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx)); + if let Some(worktree) = worktree { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + if let Some(root_entry) = worktree.root_entry() { + self.selection = Some(Selection { + worktree_id, + entry_id: root_entry.id, + }); + self.autoscroll(cx); + cx.notify(); + } + } + } + + fn autoscroll(&mut self, cx: &mut ViewContext) { + if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { + self.list.scroll_to_item(index); + cx.notify(); + } + } + + fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + self.clipboard_entry = Some(ClipboardEntry::Cut { + worktree_id: worktree.id(), + entry_id: entry.id, + }); + cx.notify(); + } + } + + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + self.clipboard_entry = Some(ClipboardEntry::Copied { + worktree_id: worktree.id(), + entry_id: entry.id, + }); + cx.notify(); + } + } + + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) -> Option<()> { + if let Some((worktree, entry)) = self.selected_entry(cx) { + let clipboard_entry = self.clipboard_entry?; + if clipboard_entry.worktree_id() != worktree.id() { + return None; + } + + let clipboard_entry_file_name = self + .project + .read(cx) + .path_for_entry(clipboard_entry.entry_id(), cx)? + .path + .file_name()? + .to_os_string(); + + let mut new_path = entry.path.to_path_buf(); + if entry.is_file() { + new_path.pop(); + } + + new_path.push(&clipboard_entry_file_name); + let extension = new_path.extension().map(|e| e.to_os_string()); + let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?; + let mut ix = 0; + while worktree.entry_for_path(&new_path).is_some() { + new_path.pop(); + + let mut new_file_name = file_name_without_extension.to_os_string(); + new_file_name.push(" copy"); + if ix > 0 { + new_file_name.push(format!(" {}", ix)); + } + if let Some(extension) = extension.as_ref() { + new_file_name.push("."); + new_file_name.push(extension); + } + + new_path.push(new_file_name); + ix += 1; + } + + if clipboard_entry.is_cut() { + if let Some(task) = self.project.update(cx, |project, cx| { + project.rename_entry(clipboard_entry.entry_id(), new_path, cx) + }) { + task.detach_and_log_err(cx) + } + } else if let Some(task) = self.project.update(cx, |project, cx| { + project.copy_entry(clipboard_entry.entry_id(), new_path, cx) + }) { + task.detach_and_log_err(cx) + } + } + None + } + + fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + cx.write_to_clipboard(ClipboardItem::new( + worktree + .abs_path() + .join(&entry.path) + .to_string_lossy() + .to_string(), + )); + } + } + + fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext) { + if let Some((_, entry)) = self.selected_entry(cx) { + cx.write_to_clipboard(ClipboardItem::new(entry.path.to_string_lossy().to_string())); + } + } + + fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + cx.reveal_path(&worktree.abs_path().join(&entry.path)); + } + } + + fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { + todo!() + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let window = cx.window(); + // let view_id = cx.view_id(); + // let path = worktree.abs_path().join(&entry.path); + + // cx.app_context() + // .spawn(|mut cx| async move { + // window.dispatch_action( + // view_id, + // &workspace::OpenTerminal { + // working_directory: path, + // }, + // &mut cx, + // ); + // }) + // .detach(); + // } + } + + pub fn new_search_in_directory( + &mut self, + _: &NewSearchInDirectory, + cx: &mut ViewContext, + ) { + if let Some((_, entry)) = self.selected_entry(cx) { + if entry.is_dir() { + cx.emit(Event::NewSearchInDirectory { + dir_entry: entry.clone(), + }); + } + } + } + + fn move_entry( + &mut self, + entry_to_move: ProjectEntryId, + destination: ProjectEntryId, + destination_is_file: bool, + cx: &mut ViewContext, + ) { + let destination_worktree = self.project.update(cx, |project, cx| { + let entry_path = project.path_for_entry(entry_to_move, cx)?; + let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + + let mut destination_path = destination_entry_path.as_ref(); + if destination_is_file { + destination_path = destination_path.parent()?; + } + + let mut new_path = destination_path.to_path_buf(); + new_path.push(entry_path.path.file_name()?); + if new_path != entry_path.path.as_ref() { + let task = project.rename_entry(entry_to_move, new_path, cx)?; + cx.foreground_executor().spawn(task).detach_and_log_err(cx); + } + + Some(project.worktree_id_for_entry(destination, cx)?) + }); + + if let Some(destination_worktree) = destination_worktree { + self.expand_entry(destination_worktree, destination, cx); + } + } + + fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { + let mut entry_index = 0; + let mut visible_entries_index = 0; + for (worktree_index, (worktree_id, worktree_entries)) in + self.visible_entries.iter().enumerate() + { + if *worktree_id == selection.worktree_id { + for entry in worktree_entries { + if entry.id == selection.entry_id { + return Some((worktree_index, entry_index, visible_entries_index)); + } else { + visible_entries_index += 1; + entry_index += 1; + } + } + break; + } else { + visible_entries_index += worktree_entries.len(); + } + } + None + } + + pub fn selected_entry<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(&'a Worktree, &'a project::Entry)> { + let (worktree, entry) = self.selected_entry_handle(cx)?; + Some((worktree.read(cx), entry)) + } + + fn selected_entry_handle<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(Model, &'a project::Entry)> { + let selection = self.selection?; + let project = self.project.read(cx); + let worktree = project.worktree_for_id(selection.worktree_id, cx)?; + let entry = worktree.read(cx).entry_for_id(selection.entry_id)?; + Some((worktree, entry)) + } + + fn expand_to_selection(&mut self, cx: &mut ViewContext) -> Option<()> { + let (worktree, entry) = self.selected_entry(cx)?; + let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default(); + + for path in entry.path.ancestors() { + let Some(entry) = worktree.entry_for_path(path) else { + continue; + }; + if entry.is_dir() { + if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(idx, entry.id); + } + } + } + + Some(()) + } + + fn update_visible_entries( + &mut self, + new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, + cx: &mut ViewContext, + ) { + let project = self.project.read(cx); + self.last_worktree_root_id = project + .visible_worktrees(cx) + .rev() + .next() + .and_then(|worktree| worktree.read(cx).root_entry()) + .map(|entry| entry.id); + + self.visible_entries.clear(); + for worktree in project.visible_worktrees(cx) { + let snapshot = worktree.read(cx).snapshot(); + let worktree_id = snapshot.id(); + + let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) { + hash_map::Entry::Occupied(e) => e.into_mut(), + hash_map::Entry::Vacant(e) => { + // The first time a worktree's root entry becomes available, + // mark that root entry as expanded. + if let Some(entry) = snapshot.root_entry() { + e.insert(vec![entry.id]).as_slice() + } else { + &[] + } + } + }; + + let mut new_entry_parent_id = None; + let mut new_entry_kind = EntryKind::Dir; + if let Some(edit_state) = &self.edit_state { + if edit_state.worktree_id == worktree_id && edit_state.is_new_entry { + new_entry_parent_id = Some(edit_state.entry_id); + new_entry_kind = if edit_state.is_dir { + EntryKind::Dir + } else { + EntryKind::File(Default::default()) + }; + } + } + + let mut visible_worktree_entries = Vec::new(); + let mut entry_iter = snapshot.entries(true); + + while let Some(entry) = entry_iter.entry() { + visible_worktree_entries.push(entry.clone()); + if Some(entry.id) == new_entry_parent_id { + visible_worktree_entries.push(Entry { + id: NEW_ENTRY_ID, + kind: new_entry_kind, + path: entry.path.join("\0").into(), + inode: 0, + mtime: entry.mtime, + is_symlink: false, + is_ignored: false, + is_external: false, + git_status: entry.git_status, + }); + } + if expanded_dir_ids.binary_search(&entry.id).is_err() + && entry_iter.advance_to_sibling() + { + continue; + } + entry_iter.advance(); + } + + snapshot.propagate_git_statuses(&mut visible_worktree_entries); + + visible_worktree_entries.sort_by(|entry_a, entry_b| { + let mut components_a = entry_a.path.components().peekable(); + let mut components_b = entry_b.path.components().peekable(); + loop { + match (components_a.next(), components_b.next()) { + (Some(component_a), Some(component_b)) => { + let a_is_file = components_a.peek().is_none() && entry_a.is_file(); + let b_is_file = components_b.peek().is_none() && entry_b.is_file(); + let ordering = a_is_file.cmp(&b_is_file).then_with(|| { + let name_a = + UniCase::new(component_a.as_os_str().to_string_lossy()); + let name_b = + UniCase::new(component_b.as_os_str().to_string_lossy()); + name_a.cmp(&name_b) + }); + if !ordering.is_eq() { + return ordering; + } + } + (Some(_), None) => break Ordering::Greater, + (None, Some(_)) => break Ordering::Less, + (None, None) => break Ordering::Equal, + } + } + }); + self.visible_entries + .push((worktree_id, visible_worktree_entries)); + } + + if let Some((worktree_id, entry_id)) = new_selected_entry { + self.selection = Some(Selection { + worktree_id, + entry_id, + }); + } + } + + fn expand_entry( + &mut self, + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) { + self.project.update(cx, |project, cx| { + if let Some((worktree, expanded_dir_ids)) = project + .worktree_for_id(worktree_id, cx) + .zip(self.expanded_dir_ids.get_mut(&worktree_id)) + { + project.expand_entry(worktree_id, entry_id, cx); + let worktree = worktree.read(cx); + + if let Some(mut entry) = worktree.entry_for_id(entry_id) { + loop { + if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(ix, entry.id); + } + + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } + } + } + } + }); + } + + fn for_each_visible_entry( + &self, + range: Range, + cx: &mut ViewContext, + mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext), + ) { + let mut ix = 0; + for (worktree_id, visible_worktree_entries) in &self.visible_entries { + if ix >= range.end { + return; + } + + if ix + visible_worktree_entries.len() <= range.start { + ix += visible_worktree_entries.len(); + continue; + } + + let end_ix = range.end.min(ix + visible_worktree_entries.len()); + let (git_status_setting, show_file_icons, show_folder_icons) = { + let settings = ProjectPanelSettings::get_global(cx); + ( + settings.git_status, + settings.file_icons, + settings.folder_icons, + ) + }; + if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { + let snapshot = worktree.read(cx).snapshot(); + let root_name = OsStr::new(snapshot.root_name()); + let expanded_entry_ids = self + .expanded_dir_ids + .get(&snapshot.id()) + .map(Vec::as_slice) + .unwrap_or(&[]); + + let entry_range = range.start.saturating_sub(ix)..end_ix - ix; + for entry in visible_worktree_entries[entry_range].iter() { + let status = git_status_setting.then(|| entry.git_status).flatten(); + let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); + let icon = match entry.kind { + EntryKind::File(_) => { + if show_file_icons { + Some(FileAssociations::get_icon(&entry.path, cx)) + } else { + None + } + } + _ => { + if show_folder_icons { + Some(FileAssociations::get_folder_icon(is_expanded, cx)) + } else { + Some(FileAssociations::get_chevron_icon(is_expanded, cx)) + } + } + }; + + let mut details = EntryDetails { + filename: entry + .path + .file_name() + .unwrap_or(root_name) + .to_string_lossy() + .to_string(), + icon, + path: entry.path.clone(), + depth: entry.path.components().count(), + kind: entry.kind, + is_ignored: entry.is_ignored, + is_expanded, + is_selected: self.selection.map_or(false, |e| { + e.worktree_id == snapshot.id() && e.entry_id == entry.id + }), + is_editing: false, + is_processing: false, + is_cut: self + .clipboard_entry + .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), + git_status: status, + }; + + if let Some(edit_state) = &self.edit_state { + let is_edited_entry = if edit_state.is_new_entry { + entry.id == NEW_ENTRY_ID + } else { + entry.id == edit_state.entry_id + }; + + if is_edited_entry { + if let Some(processing_filename) = &edit_state.processing_filename { + details.is_processing = true; + details.filename.clear(); + details.filename.push_str(processing_filename); + } else { + if edit_state.is_new_entry { + details.filename.clear(); + } + details.is_editing = true; + } + } + } + + callback(entry.id, details, cx); + } + } + ix = end_ix; + } + } + + fn render_entry_visual_element( + details: &EntryDetails, + editor: Option<&View>, + padding: Pixels, + cx: &mut ViewContext, + ) -> Div { + let show_editor = details.is_editing && !details.is_processing; + + let theme = cx.theme(); + let filename_text_color = details + .git_status + .as_ref() + .map(|status| match status { + GitFileStatus::Added => theme.styles.status.created, + GitFileStatus::Modified => theme.styles.status.modified, + GitFileStatus::Conflict => theme.styles.status.conflict, + }) + .unwrap_or(theme.styles.status.info); + + h_stack() + .child(if let Some(icon) = &details.icon { + div().child(svg().path(icon.to_string())) + } else { + div() + }) + .child( + if let (Some(editor), true) = (editor, show_editor) { + div().child(editor.clone()) + } else { + div().child(details.filename.clone()) + } + .ml_1(), + ) + .pl(padding) + } + + fn render_entry( + entry_id: ProjectEntryId, + details: EntryDetails, + editor: &View, + // dragged_entry_destination: &mut Option>, + // theme: &theme::ProjectPanel, + cx: &mut ViewContext, + ) -> Div> { + let kind = details.kind; + let settings = ProjectPanelSettings::get_global(cx); + const INDENT_SIZE: Pixels = px(16.0); + let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); + let show_editor = details.is_editing && !details.is_processing; + + Self::render_entry_visual_element(&details, Some(editor), padding, cx) + .id(entry_id.to_proto() as usize) + .on_click(move |this, event, cx| { + if !show_editor { + if kind.is_dir() { + this.toggle_expanded(entry_id, cx); + } else { + if event.down.modifiers.command { + this.split_entry(entry_id, cx); + } else { + this.open_entry(entry_id, event.up.click_count > 1, cx); + } + } + } + }) + // .on_down(MouseButton::Right, move |event, this, cx| { + // this.deploy_context_menu(event.position, entry_id, cx); + // }) + // .on_up(MouseButton::Left, move |_, this, cx| { + // if let Some((_, dragged_entry)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // this.move_entry( + // *dragged_entry, + // entry_id, + // matches!(details.kind, EntryKind::File(_)), + // cx, + // ); + // } + // }) + } +} + +impl Render for ProjectPanel { + type Element = Div>; + + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { + enum ProjectPanel {} + let theme = cx.theme(); + let last_worktree_root_id = self.last_worktree_root_id; + + let has_worktree = self.visible_entries.len() != 0; + + if has_worktree { + div().id("project-panel").child( + uniform_list( + "entries", + self.visible_entries + .iter() + .map(|(_, worktree_entries)| worktree_entries.len()) + .sum(), + |this: &mut Self, range, cx| { + let mut items = SmallVec::new(); + this.for_each_visible_entry(range, cx, |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + // &mut dragged_entry_destination, + cx, + )); + }); + items + }, + ) + .track_scroll(self.list.clone()), + ) + } else { + v_stack().id("empty-project_panel") + } + } +} + +impl EventEmitter for ProjectPanel {} + +impl EventEmitter for ProjectPanel {} + +impl workspace::dock::Panel for ProjectPanel { + fn position(&self, cx: &WindowContext) -> DockPosition { + match ProjectPanelSettings::get_global(cx).dock { + ProjectPanelDockPosition::Left => DockPosition::Left, + ProjectPanelDockPosition::Right => DockPosition::Right, + } + } + + fn position_is_valid(&self, position: DockPosition) -> bool { + matches!(position, DockPosition::Left | DockPosition::Right) + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings| { + let dock = match position { + DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left, + DockPosition::Right => ProjectPanelDockPosition::Right, + }; + settings.dock = Some(dock); + }, + ); + } + + fn size(&self, cx: &WindowContext) -> f32 { + self.width + .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width) + } + + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + self.width = size; + self.serialize(cx); + cx.notify(); + } + + fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { + Some("icons/project.svg") + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Project Panel".into(), Some(Box::new(ToggleFocus))) + } + + // fn should_change_position_on_event(event: &Self::Event) -> bool { + // matches!(event, Event::DockPositionChanged) + // } + + fn has_focus(&self, _: &WindowContext) -> bool { + self.has_focus + } + + fn persistent_name(&self) -> &'static str { + "Project Panel" + } + + // fn is_focus_event(event: &Self::Event) -> bool { + // matches!(event, Event::Focus) + // } +} + +impl ClipboardEntry { + fn is_cut(&self) -> bool { + matches!(self, Self::Cut { .. }) + } + + fn entry_id(&self) -> ProjectEntryId { + match self { + ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => { + *entry_id + } + } + } + + fn worktree_id(&self) -> WorktreeId { + match self { + ClipboardEntry::Copied { worktree_id, .. } + | ClipboardEntry::Cut { worktree_id, .. } => *worktree_id, + } + } +} + +// todo!() +// #[cfg(test)] +// mod tests { +// use super::*; +// use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle}; +// use pretty_assertions::assert_eq; +// use project::FakeFs; +// use serde_json::json; +// use settings::SettingsStore; +// use std::{ +// collections::HashSet, +// path::{Path, PathBuf}, +// sync::atomic::{self, AtomicUsize}, +// }; +// use workspace::{pane, AppState}; + +// #[gpui::test] +// async fn test_visible_list(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.executor().clone()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// toggle_expand_dir(&panel, "root1/b", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b <== selected", +// " > 3", +// " > 4", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// assert_eq!( +// visible_entries_as_strings(&panel, 6..9, cx), +// &[ +// // +// " > C", +// " .dockerignore", +// "v root2", +// ] +// ); +// } + +// #[gpui::test(iterations = 30)] +// async fn test_editing_files(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// select_path(&panel, "root1", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1 <== selected", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// // Add a file with the root folder selected. The filename editor is placed +// // before the first file in the root folder. +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [EDITOR: ''] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); +// panel.confirm(&Confirm, cx).unwrap() +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [PROCESSING: 'the-new-filename'] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// " the-new-filename <== selected", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// select_path(&panel, "root1/b", cx); +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: ''] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel +// .update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx)); +// panel.confirm(&Confirm, cx).unwrap() +// }) +// .await +// .unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " another-filename.txt <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// select_path(&panel, "root1/b/another-filename.txt", cx); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: 'another-filename.txt'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// let file_name_selections = editor.selections.all::(cx); +// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); +// let file_name_selection = &file_name_selections[0]; +// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); +// assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension"); + +// editor.set_text("a-different-filename.tar.gz", cx) +// }); +// panel.confirm(&Confirm, cx).unwrap() +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [PROCESSING: 'a-different-filename.tar.gz'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " a-different-filename.tar.gz <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: 'a-different-filename.tar.gz'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// let file_name_selections = editor.selections.all::(cx); +// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); +// let file_name_selection = &file_name_selections[0]; +// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); +// assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot"); + +// }); +// panel.cancel(&Cancel, cx) +// }); + +// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > [EDITOR: ''] <== selected", +// " > 3", +// " > 4", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("new-dir", cx)); +// panel.confirm(&Confirm, cx).unwrap() +// }); +// panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > [PROCESSING: 'new-dir']", +// " > 3 <== selected", +// " > 4", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3 <== selected", +// " > 4", +// " > new-dir", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > [EDITOR: '3'] <== selected", +// " > 4", +// " > new-dir", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// // Dismiss the rename editor when it loses focus. +// workspace.update(cx, |_, cx| cx.focus_self()); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3 <== selected", +// " > 4", +// " > new-dir", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); +// } + +// #[gpui::test(iterations = 30)] +// async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// select_path(&panel, "root1", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1 <== selected", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// // Add a file with the root folder selected. The filename editor is placed +// // before the first file in the root folder. +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [EDITOR: ''] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// editor.set_text("/bdir1/dir2/the-new-filename", cx) +// }); +// panel.confirm(&Confirm, cx).unwrap() +// }); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..13, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " v bdir1", +// " v dir2", +// " the-new-filename <== selected", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); +// } + +// #[gpui::test] +// async fn test_copy_paste(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// "one.two.txt": "", +// "one.txt": "" +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// panel.update(cx, |panel, cx| { +// panel.select_next(&Default::default(), cx); +// panel.select_next(&Default::default(), cx); +// }); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// // +// "v root1", +// " one.two.txt <== selected", +// " one.txt", +// ] +// ); + +// // Regression test - file name is created correctly when +// // the copied file's name contains multiple dots. +// panel.update(cx, |panel, cx| { +// panel.copy(&Default::default(), cx); +// panel.paste(&Default::default(), cx); +// }); +// cx.foreground().run_until_parked(); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// // +// "v root1", +// " one.two copy.txt", +// " one.two.txt <== selected", +// " one.txt", +// ] +// ); + +// panel.update(cx, |panel, cx| { +// panel.paste(&Default::default(), cx); +// }); +// cx.foreground().run_until_parked(); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// // +// "v root1", +// " one.two copy 1.txt", +// " one.two copy.txt", +// " one.two.txt <== selected", +// " one.txt", +// ] +// ); +// } + +// #[gpui::test] +// async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// toggle_expand_dir(&panel, "src/test", cx); +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ] +// ); +// ensure_single_file_is_opened(window, "test/first.rs", cx); + +// submit_deletion(window.into(), &panel, cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " second.rs", +// " third.rs" +// ], +// "Project panel should have no deleted file, no other file is selected in it" +// ); +// ensure_no_open_items_and_panes(window.into(), &workspace, cx); + +// select_path(&panel, "src/test/second.rs", cx); +// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " second.rs <== selected", +// " third.rs" +// ] +// ); +// ensure_single_file_is_opened(window, "test/second.rs", cx); + +// window.update(cx, |cx| { +// let active_items = workspace +// .read(cx) +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()) +// .collect::>(); +// assert_eq!(active_items.len(), 1); +// let open_editor = active_items +// .into_iter() +// .next() +// .unwrap() +// .downcast::() +// .expect("Open item should be an editor"); +// open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); +// }); +// submit_deletion(window.into(), &panel, cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " v test", " third.rs"], +// "Project panel should have no deleted file, with one last file remaining" +// ); +// ensure_no_open_items_and_panes(window.into(), &workspace, cx); +// } + +// #[gpui::test] +// async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// select_path(&panel, "src/", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src <== selected", " > test"] +// ); +// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " > [EDITOR: ''] <== selected", " > test"] +// ); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("test", cx)); +// assert!( +// panel.confirm(&Confirm, cx).is_none(), +// "Should not allow to confirm on conflicting new directory name" +// ) +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " > test"], +// "File list should be unchanged after failed folder create confirmation" +// ); + +// select_path(&panel, "src/test/", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " > test <== selected"] +// ); +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " [EDITOR: ''] <== selected", +// " first.rs", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("first.rs", cx)); +// assert!( +// panel.confirm(&Confirm, cx).is_none(), +// "Should not allow to confirm on conflicting new file name" +// ) +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs", +// " second.rs", +// " third.rs" +// ], +// "File list should be unchanged after failed file create confirmation" +// ); + +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ], +// ); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " [EDITOR: 'first.rs'] <== selected", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("second.rs", cx)); +// assert!( +// panel.confirm(&Confirm, cx).is_none(), +// "Should not allow to confirm on conflicting file rename" +// ) +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ], +// "File list should be unchanged after failed rename confirmation" +// ); +// } + +// #[gpui::test] +// async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// let new_search_events_count = Arc::new(AtomicUsize::new(0)); +// let _subscription = panel.update(cx, |_, cx| { +// let subcription_count = Arc::clone(&new_search_events_count); +// cx.subscribe(&cx.handle(), move |_, _, event, _| { +// if matches!(event, Event::NewSearchInDirectory { .. }) { +// subcription_count.fetch_add(1, atomic::Ordering::SeqCst); +// } +// }) +// }); + +// toggle_expand_dir(&panel, "src/test", cx); +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel.new_search_in_directory(&NewSearchInDirectory, cx) +// }); +// assert_eq!( +// new_search_events_count.load(atomic::Ordering::SeqCst), +// 0, +// "Should not trigger new search in directory when called on a file" +// ); + +// select_path(&panel, "src/test", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test <== selected", +// " first.rs", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel.new_search_in_directory(&NewSearchInDirectory, cx) +// }); +// assert_eq!( +// new_search_events_count.load(atomic::Ordering::SeqCst), +// 1, +// "Should trigger new search in directory when called on a directory" +// ); +// } + +// #[gpui::test] +// async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/project_root", +// json!({ +// "dir_1": { +// "nested_dir": { +// "file_a.py": "# File contents", +// "file_b.py": "# File contents", +// "file_c.py": "# File contents", +// }, +// "file_1.py": "# File contents", +// "file_2.py": "# File contents", +// "file_3.py": "# File contents", +// }, +// "dir_2": { +// "file_1.py": "# File contents", +// "file_2.py": "# File contents", +// "file_3.py": "# File contents", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// panel.update(cx, |panel, cx| { +// panel.collapse_all_entries(&CollapseAllEntries, cx) +// }); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v project_root", " > dir_1", " > dir_2",] +// ); + +// // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries +// toggle_expand_dir(&panel, "project_root/dir_1", cx); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v project_root", +// " v dir_1 <== selected", +// " > nested_dir", +// " file_1.py", +// " file_2.py", +// " file_3.py", +// " > dir_2", +// ] +// ); +// } + +// #[gpui::test] +// async fn test_new_file_move(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.as_fake().insert_tree("/root", json!({})).await; +// let project = Project::test(fs, ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// // Make a new buffer with no backing file +// workspace.update(cx, |workspace, cx| { +// Editor::new_file(workspace, &Default::default(), cx) +// }); + +// // "Save as"" the buffer, creating a new backing file for it +// let task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(workspace::SaveIntent::Save, cx) +// }); + +// cx.foreground().run_until_parked(); +// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); +// task.await.unwrap(); + +// // Rename the file +// select_path(&panel, "root/new", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v root", " new <== selected"] +// ); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("newer", cx)); +// }); +// panel +// .update(cx, |panel, cx| panel.confirm(&Confirm, cx)) +// .unwrap() +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v root", " newer <== selected"] +// ); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.save_active_item(workspace::SaveIntent::Save, cx) +// }) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); +// // assert that saving the file doesn't restore "new" +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v root", " newer <== selected"] +// ); +// } + +// fn toggle_expand_dir( +// panel: &View, +// path: impl AsRef, +// cx: &mut TestAppContext, +// ) { +// let path = path.as_ref(); +// panel.update(cx, |panel, cx| { +// for worktree in panel.project.read(cx).worktrees().collect::>() { +// let worktree = worktree.read(cx); +// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { +// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; +// panel.toggle_expanded(entry_id, cx); +// return; +// } +// } +// panic!("no worktree for path {:?}", path); +// }); +// } + +// fn select_path(panel: &View, path: impl AsRef, cx: &mut TestAppContext) { +// let path = path.as_ref(); +// panel.update(cx, |panel, cx| { +// for worktree in panel.project.read(cx).worktrees().collect::>() { +// let worktree = worktree.read(cx); +// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { +// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; +// panel.selection = Some(Selection { +// worktree_id: worktree.id(), +// entry_id, +// }); +// return; +// } +// } +// panic!("no worktree for path {:?}", path); +// }); +// } + +// fn visible_entries_as_strings( +// panel: &View, +// range: Range, +// cx: &mut TestAppContext, +// ) -> Vec { +// let mut result = Vec::new(); +// let mut project_entries = HashSet::new(); +// let mut has_editor = false; + +// panel.update(cx, |panel, cx| { +// panel.for_each_visible_entry(range, cx, |project_entry, details, _| { +// if details.is_editing { +// assert!(!has_editor, "duplicate editor entry"); +// has_editor = true; +// } else { +// assert!( +// project_entries.insert(project_entry), +// "duplicate project entry {:?} {:?}", +// project_entry, +// details +// ); +// } + +// let indent = " ".repeat(details.depth); +// let icon = if details.kind.is_dir() { +// if details.is_expanded { +// "v " +// } else { +// "> " +// } +// } else { +// " " +// }; +// let name = if details.is_editing { +// format!("[EDITOR: '{}']", details.filename) +// } else if details.is_processing { +// format!("[PROCESSING: '{}']", details.filename) +// } else { +// details.filename.clone() +// }; +// let selected = if details.is_selected { +// " <== selected" +// } else { +// "" +// }; +// result.push(format!("{indent}{icon}{name}{selected}")); +// }); +// }); + +// result +// } + +// fn init_test(cx: &mut TestAppContext) { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// init_settings(cx); +// theme::init(cx); +// language::init(cx); +// editor::init_settings(cx); +// crate::init((), cx); +// workspace::init_settings(cx); +// client::init_settings(cx); +// Project::init_settings(cx); +// }); +// } + +// fn init_test_with_editor(cx: &mut TestAppContext) { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// let app_state = AppState::test(cx); +// theme::init(cx); +// init_settings(cx); +// language::init(cx); +// editor::init(cx); +// pane::init(cx); +// crate::init((), cx); +// workspace::init(app_state.clone(), cx); +// Project::init_settings(cx); +// }); +// } + +// fn ensure_single_file_is_opened( +// window: WindowHandle, +// expected_path: &str, +// cx: &mut TestAppContext, +// ) { +// window.update_root(cx, |workspace, cx| { +// let worktrees = workspace.worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// let worktree_id = WorktreeId::from_usize(worktrees[0].id()); + +// let open_project_paths = workspace +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) +// .collect::>(); +// assert_eq!( +// open_project_paths, +// vec![ProjectPath { +// worktree_id, +// path: Arc::from(Path::new(expected_path)) +// }], +// "Should have opened file, selected in project panel" +// ); +// }); +// } + +// fn submit_deletion( +// window: AnyWindowHandle, +// panel: &View, +// cx: &mut TestAppContext, +// ) { +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts before the deletion" +// ); +// panel.update(cx, |panel, cx| { +// panel +// .delete(&Delete, cx) +// .expect("Deletion start") +// .detach_and_log_err(cx); +// }); +// assert!( +// window.has_pending_prompt(cx), +// "Should have a prompt after the deletion" +// ); +// window.simulate_prompt_answer(0, cx); +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts after prompt was replied to" +// ); +// cx.foreground().run_until_parked(); +// } + +// fn ensure_no_open_items_and_panes( +// window: AnyWindowHandle, +// workspace: &View, +// cx: &mut TestAppContext, +// ) { +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts after deletion operation closes the file" +// ); +// window.read_with(cx, |cx| { +// let open_project_paths = workspace +// .read(cx) +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) +// .collect::>(); +// assert!( +// open_project_paths.is_empty(), +// "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" +// ); +// }); +// } +// } diff --git a/crates/project_panel2/src/project_panel_settings.rs b/crates/project_panel2/src/project_panel_settings.rs new file mode 100644 index 0000000000..5b0e0194a5 --- /dev/null +++ b/crates/project_panel2/src/project_panel_settings.rs @@ -0,0 +1,45 @@ +use anyhow; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use settings::Settings; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProjectPanelDockPosition { + Left, + Right, +} + +#[derive(Deserialize, Debug)] +pub struct ProjectPanelSettings { + pub default_width: f32, + pub dock: ProjectPanelDockPosition, + pub file_icons: bool, + pub folder_icons: bool, + pub git_status: bool, + pub indent_size: f32, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct ProjectPanelSettingsContent { + pub default_width: Option, + pub dock: Option, + pub file_icons: Option, + pub folder_icons: Option, + pub git_status: Option, + pub indent_size: Option, +} + +impl Settings for ProjectPanelSettings { + const KEY: Option<&'static str> = Some("project_panel"); + + type FileContent = ProjectPanelSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 24ec810ac5..df2da095ee 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -29,7 +29,7 @@ use client2::{ Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; -use dock::{Dock, DockPosition, PanelButtons}; +use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle as _}; use futures::{ channel::{mpsc, oneshot}, future::try_join_all, @@ -937,108 +937,15 @@ impl Workspace { &self.right_dock } - // pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) - // where - // T::Event: std::fmt::Debug, - // { - // self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {}) - // } + pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) { + let dock = match panel.position(cx) { + DockPosition::Left => &self.left_dock, + DockPosition::Bottom => &self.bottom_dock, + DockPosition::Right => &self.right_dock, + }; - // pub fn add_panel_with_extra_event_handler( - // &mut self, - // panel: View, - // cx: &mut ViewContext, - // handler: F, - // ) where - // T::Event: std::fmt::Debug, - // F: Fn(&mut Self, &View, &T::Event, &mut ViewContext) + 'static, - // { - // let dock = match panel.position(cx) { - // DockPosition::Left => &self.left_dock, - // DockPosition::Bottom => &self.bottom_dock, - // DockPosition::Right => &self.right_dock, - // }; - - // self.subscriptions.push(cx.subscribe(&panel, { - // let mut dock = dock.clone(); - // let mut prev_position = panel.position(cx); - // move |this, panel, event, cx| { - // if T::should_change_position_on_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // let new_position = panel.read(cx).position(cx); - // let mut was_visible = false; - // dock.update(cx, |dock, cx| { - // prev_position = new_position; - - // was_visible = dock.is_open() - // && dock - // .visible_panel() - // .map_or(false, |active_panel| active_panel.id() == panel.id()); - // dock.remove_panel(&panel, cx); - // }); - - // if panel.is_zoomed(cx) { - // this.zoomed_position = Some(new_position); - // } - - // dock = match panel.read(cx).position(cx) { - // DockPosition::Left => &this.left_dock, - // DockPosition::Bottom => &this.bottom_dock, - // DockPosition::Right => &this.right_dock, - // } - // .clone(); - // dock.update(cx, |dock, cx| { - // dock.add_panel(panel.clone(), cx); - // if was_visible { - // dock.set_open(true, cx); - // dock.activate_panel(dock.panels_len() - 1, cx); - // } - // }); - // } else if T::should_zoom_in_on_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); - // if !panel.has_focus(cx) { - // cx.focus(&panel); - // } - // this.zoomed = Some(panel.downgrade().into_any()); - // this.zoomed_position = Some(panel.read(cx).position(cx)); - // } else if T::should_zoom_out_on_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx)); - // if this.zoomed_position == Some(prev_position) { - // this.zoomed = None; - // this.zoomed_position = None; - // } - // cx.notify(); - // } else if T::is_focus_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // let position = panel.read(cx).position(cx); - // this.dismiss_zoomed_items_to_reveal(Some(position), cx); - // if panel.is_zoomed(cx) { - // this.zoomed = Some(panel.downgrade().into_any()); - // this.zoomed_position = Some(position); - // } else { - // this.zoomed = None; - // this.zoomed_position = None; - // } - // this.update_active_view_for_followers(cx); - // cx.notify(); - // } else { - // handler(this, &panel, event, cx) - // } - // } - // })); - - // dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); - // } + dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); + } pub fn status_bar(&self) -> &View { &self.status_bar @@ -1727,42 +1634,42 @@ impl Workspace { // } // } - // pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { - // let dock = match dock_side { - // DockPosition::Left => &self.left_dock, - // DockPosition::Bottom => &self.bottom_dock, - // DockPosition::Right => &self.right_dock, - // }; - // let mut focus_center = false; - // let mut reveal_dock = false; - // dock.update(cx, |dock, cx| { - // let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); - // let was_visible = dock.is_open() && !other_is_zoomed; - // dock.set_open(!was_visible, cx); + pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { + let dock = match dock_side { + DockPosition::Left => &self.left_dock, + DockPosition::Bottom => &self.bottom_dock, + DockPosition::Right => &self.right_dock, + }; + let mut focus_center = false; + let mut reveal_dock = false; + dock.update(cx, |dock, cx| { + let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); + let was_visible = dock.is_open() && !other_is_zoomed; + dock.set_open(!was_visible, cx); - // if let Some(active_panel) = dock.active_panel() { - // if was_visible { - // if active_panel.has_focus(cx) { - // focus_center = true; - // } - // } else { - // cx.focus(active_panel.as_any()); - // reveal_dock = true; - // } - // } - // }); + if let Some(active_panel) = dock.active_panel() { + if was_visible { + if active_panel.has_focus(cx) { + focus_center = true; + } + } else { + // cx.focus(active_panel.as_any()); + reveal_dock = true; + } + } + }); - // if reveal_dock { - // self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx); - // } + if reveal_dock { + self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx); + } - // if focus_center { - // cx.focus_self(); - // } + if focus_center { + cx.focus(&self.focus_handle); + } - // cx.notify(); - // self.serialize_workspace(cx); - // } + cx.notify(); + self.serialize_workspace(cx); + } pub fn close_all_docks(&mut self, cx: &mut ViewContext) { let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock]; diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 661ab0c293..e03c44547b 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -55,7 +55,7 @@ node_runtime = { path = "../node_runtime" } # outline = { path = "../outline" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } -# project_panel = { path = "../project_panel" } +project_panel = { package = "project_panel2", path = "../project_panel2" } # project_symbols = { path = "../project_symbols" } # quick_action_bar = { path = "../quick_action_bar" } # recent_projects = { path = "../recent_projects" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index cd0f8e5fbf..6a76ffac48 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -191,7 +191,7 @@ fn main() { // file_finder::init(cx); // outline::init(cx); // project_symbols::init(cx); - // project_panel::init(Assets, cx); + project_panel::init(Assets, cx); // channel::init(&client, user_store.clone(), cx); // diagnostics::init(cx); // search::init(cx); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 7368d3a5ef..bc9097dd3c 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -15,9 +15,10 @@ pub use only_instance::*; pub use open_listener::*; use anyhow::Result; +use project_panel::ProjectPanel; use std::sync::Arc; use uuid::Uuid; -use workspace::{AppState, Workspace}; +use workspace::{dock::PanelHandle as _, AppState, Workspace}; pub fn build_window_options( bounds: Option, @@ -138,49 +139,38 @@ pub fn initialize_workspace( // } // false // }); - // })?; + })?; - // let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); - // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); - // let channels_panel = - // collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); - // let chat_panel = - // collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); - // let notification_panel = collab_ui::notification_panel::NotificationPanel::load( - // workspace_handle.clone(), - // cx.clone(), - // ); - // let ( - // project_panel, + let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); + // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); + // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); + // let channels_panel = + // collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); + // let chat_panel = + // collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); + // let notification_panel = collab_ui::notification_panel::NotificationPanel::load( + // workspace_handle.clone(), + // cx.clone(), + // ); + let ( + project_panel, // terminal_panel, // assistant_panel, // channels_panel, // chat_panel, // notification_panel, - // ) = futures::try_join!( - // project_panel, + ) = futures::try_join!( + project_panel, // terminal_panel, // assistant_panel, // channels_panel, // chat_panel, // notification_panel, - // )?; - // workspace_handle.update(&mut cx, |workspace, cx| { - // let project_panel_position = project_panel.position(cx); - // workspace.add_panel_with_extra_event_handler( - // project_panel, - // cx, - // |workspace, _, event, cx| match event { - // project_panel::Event::NewSearchInDirectory { dir_entry } => { - // search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx) - // } - // project_panel::Event::ActivatePanel => { - // workspace.focus_panel::(cx); - // } - // _ => {} - // }, - // ); + )?; + + workspace_handle.update(&mut cx, |workspace, cx| { + let project_panel_position = project_panel.position(cx); + workspace.add_panel(project_panel, cx); // workspace.add_panel(terminal_panel, cx); // workspace.add_panel(assistant_panel, cx); // workspace.add_panel(channels_panel, cx); @@ -198,9 +188,9 @@ pub fn initialize_workspace( // .map_or(false, |entry| entry.is_dir()) // }) // { - // workspace.toggle_dock(project_panel_position, cx); + workspace.toggle_dock(project_panel_position, cx); // } - // cx.focus_self(); + // cx.focus_self(); })?; Ok(()) }) From 74a0d9316a850f7e39c9a6a9603831129600d594 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 10 Nov 2023 11:56:14 -0700 Subject: [PATCH 010/126] Add a DispatchTree which will replace the existing key dispatch strategy Instead of freezing a stack, we will record the entire dispatch tree so we can change focus. Co-Authored-By: Antonio Scandurra --- crates/editor2/src/editor.rs | 22 +- crates/editor2/src/element.rs | 28 +- crates/gpui/src/dispatch.rs | 1 + crates/gpui2/src/action.rs | 400 +------------------------ crates/gpui2/src/dispatch.rs | 225 ++++++++++++++ crates/gpui2/src/gpui2.rs | 1 + crates/gpui2/src/interactive.rs | 28 +- crates/gpui2/src/keymap/binding.rs | 12 +- crates/gpui2/src/keymap/context.rs | 434 ++++++++++++++++++++++++++++ crates/gpui2/src/keymap/keymap.rs | 4 +- crates/gpui2/src/keymap/matcher.rs | 10 +- crates/gpui2/src/keymap/mod.rs | 2 + crates/gpui2/src/window.rs | 34 +-- crates/workspace2/src/workspace2.rs | 14 +- 14 files changed, 724 insertions(+), 491 deletions(-) create mode 100644 crates/gpui/src/dispatch.rs create mode 100644 crates/gpui2/src/dispatch.rs create mode 100644 crates/gpui2/src/keymap/context.rs diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 5bc67a57b6..ad17f96055 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -41,8 +41,8 @@ use git::diff_hunk_to_display; use gpui::{ action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, - DispatchContext, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, - HighlightStyle, Hsla, InputHandler, Model, MouseButton, ParentElement, Pixels, Render, + EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, + InputHandler, KeyBindingContext, Model, MouseButton, ParentElement, Pixels, Render, StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -646,7 +646,7 @@ pub struct Editor { collapse_matches: bool, autoindent_mode: Option, workspace: Option<(WeakView, i64)>, - keymap_context_layers: BTreeMap, + keymap_context_layers: BTreeMap, input_enabled: bool, read_only: bool, leader_peer_id: Option, @@ -1980,9 +1980,9 @@ impl Editor { this } - fn dispatch_context(&self, cx: &AppContext) -> DispatchContext { - let mut dispatch_context = DispatchContext::default(); - dispatch_context.insert("Editor"); + fn dispatch_context(&self, cx: &AppContext) -> KeyBindingContext { + let mut dispatch_context = KeyBindingContext::default(); + dispatch_context.add("Editor"); let mode = match self.mode { EditorMode::SingleLine => "single_line", EditorMode::AutoHeight { .. } => "auto_height", @@ -1990,17 +1990,17 @@ impl Editor { }; dispatch_context.set("mode", mode); if self.pending_rename.is_some() { - dispatch_context.insert("renaming"); + dispatch_context.add("renaming"); } if self.context_menu_visible() { match self.context_menu.read().as_ref() { Some(ContextMenu::Completions(_)) => { - dispatch_context.insert("menu"); - dispatch_context.insert("showing_completions") + dispatch_context.add("menu"); + dispatch_context.add("showing_completions") } Some(ContextMenu::CodeActions(_)) => { - dispatch_context.insert("menu"); - dispatch_context.insert("showing_code_actions") + dispatch_context.add("menu"); + dispatch_context.add("showing_code_actions") } None => {} } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 67fcbaa4ba..2cd319f66b 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -16,11 +16,12 @@ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, - BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchContext, DispatchPhase, - Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, - InputHandler, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size, - Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout, + BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, + ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, + KeyBindingContext, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, + ShapedGlyph, Size, Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, + WrappedLineLayout, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -4157,21 +4158,6 @@ fn build_key_listeners( build_action_listener(Editor::context_menu_prev), build_action_listener(Editor::context_menu_next), build_action_listener(Editor::context_menu_last), - build_key_listener( - move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| { - if phase == DispatchPhase::Bubble { - if let KeyMatch::Some(action) = cx.match_keystroke( - &global_element_id, - &key_down.keystroke, - dispatch_context, - ) { - return Some(action); - } - } - - None - }, - ), ] } @@ -4179,7 +4165,7 @@ fn build_key_listener( listener: impl Fn( &mut Editor, &T, - &[&DispatchContext], + &[&KeyBindingContext], DispatchPhase, &mut ViewContext, ) -> Option> diff --git a/crates/gpui/src/dispatch.rs b/crates/gpui/src/dispatch.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/gpui/src/dispatch.rs @@ -0,0 +1 @@ + diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 170ddf942f..6526f96cb9 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -1,6 +1,6 @@ use crate::SharedString; use anyhow::{anyhow, Context, Result}; -use collections::{HashMap, HashSet}; +use collections::HashMap; use lazy_static::lazy_static; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use serde::Deserialize; @@ -186,401 +186,3 @@ macro_rules! actions { actions!($($rest)*); }; } - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct DispatchContext { - set: HashSet, - map: HashMap, -} - -impl<'a> TryFrom<&'a str> for DispatchContext { - type Error = anyhow::Error; - - fn try_from(value: &'a str) -> Result { - Self::parse(value) - } -} - -impl DispatchContext { - pub fn parse(source: &str) -> Result { - let mut context = Self::default(); - let source = skip_whitespace(source); - Self::parse_expr(&source, &mut context)?; - Ok(context) - } - - fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> { - if source.is_empty() { - return Ok(()); - } - - let key = source - .chars() - .take_while(|c| is_identifier_char(*c)) - .collect::(); - source = skip_whitespace(&source[key.len()..]); - if let Some(suffix) = source.strip_prefix('=') { - source = skip_whitespace(suffix); - let value = source - .chars() - .take_while(|c| is_identifier_char(*c)) - .collect::(); - source = skip_whitespace(&source[value.len()..]); - context.set(key, value); - } else { - context.insert(key); - } - - Self::parse_expr(source, context) - } - - pub fn is_empty(&self) -> bool { - self.set.is_empty() && self.map.is_empty() - } - - pub fn clear(&mut self) { - self.set.clear(); - self.map.clear(); - } - - pub fn extend(&mut self, other: &Self) { - for v in &other.set { - self.set.insert(v.clone()); - } - for (k, v) in &other.map { - self.map.insert(k.clone(), v.clone()); - } - } - - pub fn insert>(&mut self, identifier: I) { - self.set.insert(identifier.into()); - } - - pub fn set, S2: Into>(&mut self, key: S1, value: S2) { - self.map.insert(key.into(), value.into()); - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum DispatchContextPredicate { - Identifier(SharedString), - Equal(SharedString, SharedString), - NotEqual(SharedString, SharedString), - Child(Box, Box), - Not(Box), - And(Box, Box), - Or(Box, Box), -} - -impl DispatchContextPredicate { - pub fn parse(source: &str) -> Result { - let source = skip_whitespace(source); - let (predicate, rest) = Self::parse_expr(source, 0)?; - if let Some(next) = rest.chars().next() { - Err(anyhow!("unexpected character {next:?}")) - } else { - Ok(predicate) - } - } - - pub fn eval(&self, contexts: &[&DispatchContext]) -> bool { - let Some(context) = contexts.last() else { - return false; - }; - match self { - Self::Identifier(name) => context.set.contains(name), - Self::Equal(left, right) => context - .map - .get(left) - .map(|value| value == right) - .unwrap_or(false), - Self::NotEqual(left, right) => context - .map - .get(left) - .map(|value| value != right) - .unwrap_or(true), - Self::Not(pred) => !pred.eval(contexts), - Self::Child(parent, child) => { - parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts) - } - Self::And(left, right) => left.eval(contexts) && right.eval(contexts), - Self::Or(left, right) => left.eval(contexts) || right.eval(contexts), - } - } - - fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> { - type Op = fn( - DispatchContextPredicate, - DispatchContextPredicate, - ) -> Result; - - let (mut predicate, rest) = Self::parse_primary(source)?; - source = rest; - - 'parse: loop { - for (operator, precedence, constructor) in [ - (">", PRECEDENCE_CHILD, Self::new_child as Op), - ("&&", PRECEDENCE_AND, Self::new_and as Op), - ("||", PRECEDENCE_OR, Self::new_or as Op), - ("==", PRECEDENCE_EQ, Self::new_eq as Op), - ("!=", PRECEDENCE_EQ, Self::new_neq as Op), - ] { - if source.starts_with(operator) && precedence >= min_precedence { - source = skip_whitespace(&source[operator.len()..]); - let (right, rest) = Self::parse_expr(source, precedence + 1)?; - predicate = constructor(predicate, right)?; - source = rest; - continue 'parse; - } - } - break; - } - - Ok((predicate, source)) - } - - fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> { - let next = source - .chars() - .next() - .ok_or_else(|| anyhow!("unexpected eof"))?; - match next { - '(' => { - source = skip_whitespace(&source[1..]); - let (predicate, rest) = Self::parse_expr(source, 0)?; - if rest.starts_with(')') { - source = skip_whitespace(&rest[1..]); - Ok((predicate, source)) - } else { - Err(anyhow!("expected a ')'")) - } - } - '!' => { - let source = skip_whitespace(&source[1..]); - let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?; - Ok((DispatchContextPredicate::Not(Box::new(predicate)), source)) - } - _ if is_identifier_char(next) => { - let len = source - .find(|c: char| !is_identifier_char(c)) - .unwrap_or(source.len()); - let (identifier, rest) = source.split_at(len); - source = skip_whitespace(rest); - Ok(( - DispatchContextPredicate::Identifier(identifier.to_string().into()), - source, - )) - } - _ => Err(anyhow!("unexpected character {next:?}")), - } - } - - fn new_or(self, other: Self) -> Result { - Ok(Self::Or(Box::new(self), Box::new(other))) - } - - fn new_and(self, other: Self) -> Result { - Ok(Self::And(Box::new(self), Box::new(other))) - } - - fn new_child(self, other: Self) -> Result { - Ok(Self::Child(Box::new(self), Box::new(other))) - } - - fn new_eq(self, other: Self) -> Result { - if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { - Ok(Self::Equal(left, right)) - } else { - Err(anyhow!("operands must be identifiers")) - } - } - - fn new_neq(self, other: Self) -> Result { - if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { - Ok(Self::NotEqual(left, right)) - } else { - Err(anyhow!("operands must be identifiers")) - } - } -} - -const PRECEDENCE_CHILD: u32 = 1; -const PRECEDENCE_OR: u32 = 2; -const PRECEDENCE_AND: u32 = 3; -const PRECEDENCE_EQ: u32 = 4; -const PRECEDENCE_NOT: u32 = 5; - -fn is_identifier_char(c: char) -> bool { - c.is_alphanumeric() || c == '_' || c == '-' -} - -fn skip_whitespace(source: &str) -> &str { - let len = source - .find(|c: char| !c.is_whitespace()) - .unwrap_or(source.len()); - &source[len..] -} - -#[cfg(test)] -mod tests { - use super::*; - use crate as gpui; - use DispatchContextPredicate::*; - - #[test] - fn test_actions_definition() { - { - actions!(A, B, C, D, E, F, G); - } - - { - actions!( - A, - B, - C, - D, - E, - F, - G, // Don't wrap, test the trailing comma - ); - } - } - - #[test] - fn test_parse_context() { - let mut expected = DispatchContext::default(); - expected.set("foo", "bar"); - expected.insert("baz"); - assert_eq!(DispatchContext::parse("baz foo=bar").unwrap(), expected); - assert_eq!(DispatchContext::parse("foo = bar baz").unwrap(), expected); - assert_eq!( - DispatchContext::parse(" baz foo = bar baz").unwrap(), - expected - ); - assert_eq!(DispatchContext::parse(" foo = bar baz").unwrap(), expected); - } - - #[test] - fn test_parse_identifiers() { - // Identifiers - assert_eq!( - DispatchContextPredicate::parse("abc12").unwrap(), - Identifier("abc12".into()) - ); - assert_eq!( - DispatchContextPredicate::parse("_1a").unwrap(), - Identifier("_1a".into()) - ); - } - - #[test] - fn test_parse_negations() { - assert_eq!( - DispatchContextPredicate::parse("!abc").unwrap(), - Not(Box::new(Identifier("abc".into()))) - ); - assert_eq!( - DispatchContextPredicate::parse(" ! ! abc").unwrap(), - Not(Box::new(Not(Box::new(Identifier("abc".into()))))) - ); - } - - #[test] - fn test_parse_equality_operators() { - assert_eq!( - DispatchContextPredicate::parse("a == b").unwrap(), - Equal("a".into(), "b".into()) - ); - assert_eq!( - DispatchContextPredicate::parse("c!=d").unwrap(), - NotEqual("c".into(), "d".into()) - ); - assert_eq!( - DispatchContextPredicate::parse("c == !d") - .unwrap_err() - .to_string(), - "operands must be identifiers" - ); - } - - #[test] - fn test_parse_boolean_operators() { - assert_eq!( - DispatchContextPredicate::parse("a || b").unwrap(), - Or( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())) - ) - ); - assert_eq!( - DispatchContextPredicate::parse("a || !b && c").unwrap(), - Or( - Box::new(Identifier("a".into())), - Box::new(And( - Box::new(Not(Box::new(Identifier("b".into())))), - Box::new(Identifier("c".into())) - )) - ) - ); - assert_eq!( - DispatchContextPredicate::parse("a && b || c&&d").unwrap(), - Or( - Box::new(And( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())) - )), - Box::new(And( - Box::new(Identifier("c".into())), - Box::new(Identifier("d".into())) - )) - ) - ); - assert_eq!( - DispatchContextPredicate::parse("a == b && c || d == e && f").unwrap(), - Or( - Box::new(And( - Box::new(Equal("a".into(), "b".into())), - Box::new(Identifier("c".into())) - )), - Box::new(And( - Box::new(Equal("d".into(), "e".into())), - Box::new(Identifier("f".into())) - )) - ) - ); - assert_eq!( - DispatchContextPredicate::parse("a && b && c && d").unwrap(), - And( - Box::new(And( - Box::new(And( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())) - )), - Box::new(Identifier("c".into())), - )), - Box::new(Identifier("d".into())) - ), - ); - } - - #[test] - fn test_parse_parenthesized_expressions() { - assert_eq!( - DispatchContextPredicate::parse("a && (b == c || d != e)").unwrap(), - And( - Box::new(Identifier("a".into())), - Box::new(Or( - Box::new(Equal("b".into(), "c".into())), - Box::new(NotEqual("d".into(), "e".into())), - )), - ), - ); - assert_eq!( - DispatchContextPredicate::parse(" ( a || b ) ").unwrap(), - Or( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())), - ) - ); - } -} diff --git a/crates/gpui2/src/dispatch.rs b/crates/gpui2/src/dispatch.rs new file mode 100644 index 0000000000..372c8c2610 --- /dev/null +++ b/crates/gpui2/src/dispatch.rs @@ -0,0 +1,225 @@ +use crate::{ + Action, DispatchPhase, FocusId, KeyBindingContext, KeyDownEvent, KeyMatch, Keymap, + KeystrokeMatcher, WindowContext, +}; +use collections::HashMap; +use parking_lot::Mutex; +use smallvec::SmallVec; +use std::{any::Any, sync::Arc}; + +// trait KeyListener -> FnMut(&E, &mut V, &mut ViewContext) +type AnyKeyListener = Box; +type AnyActionListener = Box; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct DispatchNodeId(usize); + +pub struct DispatchTree { + node_stack: Vec, + context_stack: Vec, + nodes: Vec, + focused: Option, + focusable_node_ids: HashMap, + keystroke_matchers: HashMap, KeystrokeMatcher>, + keymap: Arc>, +} + +#[derive(Default)] +pub struct DispatchNode { + key_listeners: SmallVec<[AnyKeyListener; 2]>, + action_listeners: SmallVec<[AnyActionListener; 16]>, + context: KeyBindingContext, + parent: Option, +} + +impl DispatchTree { + pub fn clear(&mut self) { + self.node_stack.clear(); + self.nodes.clear(); + } + + pub fn push_node(&mut self, context: Option, old_tree: &mut Self) { + let parent = self.node_stack.last().copied(); + let node_id = DispatchNodeId(self.nodes.len()); + self.nodes.push(DispatchNode { + parent, + ..Default::default() + }); + self.node_stack.push(node_id); + if let Some(context) = context { + self.context_stack.push(context); + if let Some((context_stack, matcher)) = old_tree + .keystroke_matchers + .remove_entry(self.context_stack.as_slice()) + { + self.keystroke_matchers.insert(context_stack, matcher); + } + } + } + + pub fn pop_node(&mut self) -> DispatchNodeId { + self.node_stack.pop().unwrap() + } + + pub fn on_key_event(&mut self, listener: AnyKeyListener) { + self.active_node().key_listeners.push(listener); + } + + pub fn on_action(&mut self, listener: AnyActionListener) { + self.active_node().action_listeners.push(listener); + } + + pub fn make_focusable(&mut self, focus_id: FocusId) { + self.focusable_node_ids + .insert(focus_id, self.active_node_id()); + } + + pub fn set_focus(&mut self, focus_id: Option) { + self.focused = focus_id; + } + + pub fn active_node(&mut self) -> &mut DispatchNode { + let node_id = self.active_node_id(); + &mut self.nodes[node_id.0] + } + + fn active_node_id(&self) -> DispatchNodeId { + *self.node_stack.last().unwrap() + } + + /// Returns the DispatchNodeIds from the root of the tree to the given target node id. + fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { + let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new(); + let mut current_node_id = Some(target); + while let Some(node_id) = current_node_id { + dispatch_path.push(node_id); + current_node_id = self.nodes[node_id.0].parent; + } + dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. + dispatch_path + } + + pub fn dispatch_key(&mut self, event: &dyn Any, cx: &mut WindowContext) { + if let Some(focused_node_id) = self + .focused + .and_then(|focus_id| self.focusable_node_ids.get(&focus_id)) + .copied() + { + self.dispatch_key_on_node(focused_node_id, event, cx); + } + } + + fn dispatch_key_on_node( + &mut self, + node_id: DispatchNodeId, + event: &dyn Any, + cx: &mut WindowContext, + ) { + let dispatch_path = self.dispatch_path(node_id); + + // Capture phase + self.context_stack.clear(); + cx.propagate_event = true; + for node_id in &dispatch_path { + let node = &self.nodes[node_id.0]; + if !node.context.is_empty() { + self.context_stack.push(node.context.clone()); + } + + for key_listener in &node.key_listeners { + key_listener(event, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = &self.nodes[node_id.0]; + + // Handle low level key events + for key_listener in &node.key_listeners { + key_listener(event, DispatchPhase::Bubble, cx); + if !cx.propagate_event { + return; + } + } + + // Match keystrokes + if !node.context.is_empty() { + if let Some(key_down_event) = event.downcast_ref::() { + if !self + .keystroke_matchers + .contains_key(self.context_stack.as_slice()) + { + let keystroke_contexts = self.context_stack.iter().cloned().collect(); + self.keystroke_matchers.insert( + keystroke_contexts, + KeystrokeMatcher::new(self.keymap.clone()), + ); + } + + if let Some(keystroke_matcher) = self + .keystroke_matchers + .get_mut(self.context_stack.as_slice()) + { + if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke( + &key_down_event.keystroke, + self.context_stack.as_slice(), + ) { + self.dispatch_action_on_node(*node_id, action, cx); + if !cx.propagate_event { + return; + } + } + } + } + + self.context_stack.pop(); + } + } + } + + pub fn dispatch_action(&self, action: Box, cx: &mut WindowContext) { + if let Some(focused_node_id) = self + .focused + .and_then(|focus_id| self.focusable_node_ids.get(&focus_id)) + .copied() + { + self.dispatch_action_on_node(focused_node_id, action, cx); + } + } + + fn dispatch_action_on_node( + &self, + node_id: DispatchNodeId, + action: Box, + cx: &mut WindowContext, + ) { + let dispatch_path = self.dispatch_path(node_id); + + // Capture phase + for node_id in &dispatch_path { + let node = &self.nodes[node_id.0]; + for action_listener in &node.action_listeners { + action_listener(&action, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = &self.nodes[node_id.0]; + for action_listener in &node.action_listeners { + cx.propagate_event = false; // Actions stop propagation by default during the bubble phase + action_listener(&action, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + } +} diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 79275005d2..42aea446f1 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -3,6 +3,7 @@ mod action; mod app; mod assets; mod color; +mod dispatch; mod element; mod elements; mod executor; diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 243eb3cb07..946a59a809 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,6 +1,6 @@ use crate::{ div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, BorrowWindow, Bounds, - Component, DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch, + Component, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyBindingContext, KeyMatch, Keystroke, Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style, StyleRefinement, Task, View, ViewContext, }; @@ -167,7 +167,7 @@ pub trait StatelessInteractive: Element { fn context(mut self, context: C) -> Self where Self: Sized, - C: TryInto, + C: TryInto, C::Error: Debug, { self.stateless_interactivity().dispatch_context = @@ -403,24 +403,6 @@ pub trait ElementInteractivity: 'static { ) -> R { if let Some(stateful) = self.as_stateful_mut() { cx.with_element_id(stateful.id.clone(), |global_id, cx| { - // In addition to any key down/up listeners registered directly on the element, - // we also add a key listener to match actions from the keymap. - stateful.key_listeners.push(( - TypeId::of::(), - Box::new(move |_, key_down, context, phase, cx| { - if phase == DispatchPhase::Bubble { - let key_down = key_down.downcast_ref::().unwrap(); - if let KeyMatch::Some(action) = - cx.match_keystroke(&global_id, &key_down.keystroke, context) - { - return Some(action); - } - } - - None - }), - )); - cx.with_key_dispatch_context(stateful.dispatch_context.clone(), |cx| { cx.with_key_listeners(mem::take(&mut stateful.key_listeners), f) }) @@ -808,7 +790,7 @@ impl ElementInteractivity for StatefulInteractivity { type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; pub struct StatelessInteractivity { - pub dispatch_context: DispatchContext, + pub dispatch_context: KeyBindingContext, pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, @@ -910,7 +892,7 @@ impl InteractiveElementState { impl Default for StatelessInteractivity { fn default() -> Self { Self { - dispatch_context: DispatchContext::default(), + dispatch_context: KeyBindingContext::default(), mouse_down_listeners: SmallVec::new(), mouse_up_listeners: SmallVec::new(), mouse_move_listeners: SmallVec::new(), @@ -1254,7 +1236,7 @@ pub type KeyListener = Box< dyn Fn( &mut V, &dyn Any, - &[&DispatchContext], + &[&KeyBindingContext], DispatchPhase, &mut ViewContext, ) -> Option> diff --git a/crates/gpui2/src/keymap/binding.rs b/crates/gpui2/src/keymap/binding.rs index 829f7a3b2c..1cf62484b9 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -1,11 +1,11 @@ -use crate::{Action, DispatchContext, DispatchContextPredicate, KeyMatch, Keystroke}; +use crate::{Action, KeyBindingContext, KeyBindingContextPredicate, KeyMatch, Keystroke}; use anyhow::Result; use smallvec::SmallVec; pub struct KeyBinding { action: Box, pub(super) keystrokes: SmallVec<[Keystroke; 2]>, - pub(super) context_predicate: Option, + pub(super) context_predicate: Option, } impl KeyBinding { @@ -15,7 +15,7 @@ impl KeyBinding { pub fn load(keystrokes: &str, action: Box, context: Option<&str>) -> Result { let context = if let Some(context) = context { - Some(DispatchContextPredicate::parse(context)?) + Some(KeyBindingContextPredicate::parse(context)?) } else { None }; @@ -32,7 +32,7 @@ impl KeyBinding { }) } - pub fn matches_context(&self, contexts: &[&DispatchContext]) -> bool { + pub fn matches_context(&self, contexts: &[KeyBindingContext]) -> bool { self.context_predicate .as_ref() .map(|predicate| predicate.eval(contexts)) @@ -42,7 +42,7 @@ impl KeyBinding { pub fn match_keystrokes( &self, pending_keystrokes: &[Keystroke], - contexts: &[&DispatchContext], + contexts: &[KeyBindingContext], ) -> KeyMatch { if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.matches_context(contexts) @@ -61,7 +61,7 @@ impl KeyBinding { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[&DispatchContext], + contexts: &[KeyBindingContext], ) -> Option> { if self.action.partial_eq(action) && self.matches_context(contexts) { Some(self.keystrokes.clone()) diff --git a/crates/gpui2/src/keymap/context.rs b/crates/gpui2/src/keymap/context.rs new file mode 100644 index 0000000000..834bd4989a --- /dev/null +++ b/crates/gpui2/src/keymap/context.rs @@ -0,0 +1,434 @@ +use crate::SharedString; +use anyhow::{anyhow, Result}; +use smallvec::SmallVec; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)] +pub struct KeyBindingContext(SmallVec<[ContextEntry; 8]>); + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +struct ContextEntry { + key: SharedString, + value: Option, +} + +impl<'a> TryFrom<&'a str> for KeyBindingContext { + type Error = anyhow::Error; + + fn try_from(value: &'a str) -> Result { + Self::parse(value) + } +} + +impl KeyBindingContext { + pub fn parse(source: &str) -> Result { + let mut context = Self::default(); + let source = skip_whitespace(source); + Self::parse_expr(&source, &mut context)?; + Ok(context) + } + + fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> { + if source.is_empty() { + return Ok(()); + } + + let key = source + .chars() + .take_while(|c| is_identifier_char(*c)) + .collect::(); + source = skip_whitespace(&source[key.len()..]); + if let Some(suffix) = source.strip_prefix('=') { + source = skip_whitespace(suffix); + let value = source + .chars() + .take_while(|c| is_identifier_char(*c)) + .collect::(); + source = skip_whitespace(&source[value.len()..]); + context.set(key, value); + } else { + context.add(key); + } + + Self::parse_expr(source, context) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn clear(&mut self) { + self.0.clear(); + } + + pub fn extend(&mut self, other: &Self) { + for entry in &other.0 { + if !self.contains(&entry.key) { + self.0.push(entry.clone()); + } + } + } + + pub fn add>(&mut self, identifier: I) { + let key = identifier.into(); + + if !self.contains(&key) { + self.0.push(ContextEntry { key, value: None }) + } + } + + pub fn set, S2: Into>(&mut self, key: S1, value: S2) { + let key = key.into(); + if !self.contains(&key) { + self.0.push(ContextEntry { + key, + value: Some(value.into()), + }) + } + } + + pub fn contains(&self, key: &str) -> bool { + self.0.iter().any(|entry| entry.key.as_ref() == key) + } + + pub fn get(&self, key: &str) -> Option<&SharedString> { + self.0 + .iter() + .find(|entry| entry.key.as_ref() == key)? + .value + .as_ref() + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum KeyBindingContextPredicate { + Identifier(SharedString), + Equal(SharedString, SharedString), + NotEqual(SharedString, SharedString), + Child( + Box, + Box, + ), + Not(Box), + And( + Box, + Box, + ), + Or( + Box, + Box, + ), +} + +impl KeyBindingContextPredicate { + pub fn parse(source: &str) -> Result { + let source = skip_whitespace(source); + let (predicate, rest) = Self::parse_expr(source, 0)?; + if let Some(next) = rest.chars().next() { + Err(anyhow!("unexpected character {next:?}")) + } else { + Ok(predicate) + } + } + + pub fn eval(&self, contexts: &[KeyBindingContext]) -> bool { + let Some(context) = contexts.last() else { + return false; + }; + match self { + Self::Identifier(name) => context.contains(name), + Self::Equal(left, right) => context + .get(left) + .map(|value| value == right) + .unwrap_or(false), + Self::NotEqual(left, right) => context + .get(left) + .map(|value| value != right) + .unwrap_or(true), + Self::Not(pred) => !pred.eval(contexts), + Self::Child(parent, child) => { + parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts) + } + Self::And(left, right) => left.eval(contexts) && right.eval(contexts), + Self::Or(left, right) => left.eval(contexts) || right.eval(contexts), + } + } + + fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> { + type Op = fn( + KeyBindingContextPredicate, + KeyBindingContextPredicate, + ) -> Result; + + let (mut predicate, rest) = Self::parse_primary(source)?; + source = rest; + + 'parse: loop { + for (operator, precedence, constructor) in [ + (">", PRECEDENCE_CHILD, Self::new_child as Op), + ("&&", PRECEDENCE_AND, Self::new_and as Op), + ("||", PRECEDENCE_OR, Self::new_or as Op), + ("==", PRECEDENCE_EQ, Self::new_eq as Op), + ("!=", PRECEDENCE_EQ, Self::new_neq as Op), + ] { + if source.starts_with(operator) && precedence >= min_precedence { + source = skip_whitespace(&source[operator.len()..]); + let (right, rest) = Self::parse_expr(source, precedence + 1)?; + predicate = constructor(predicate, right)?; + source = rest; + continue 'parse; + } + } + break; + } + + Ok((predicate, source)) + } + + fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> { + let next = source + .chars() + .next() + .ok_or_else(|| anyhow!("unexpected eof"))?; + match next { + '(' => { + source = skip_whitespace(&source[1..]); + let (predicate, rest) = Self::parse_expr(source, 0)?; + if rest.starts_with(')') { + source = skip_whitespace(&rest[1..]); + Ok((predicate, source)) + } else { + Err(anyhow!("expected a ')'")) + } + } + '!' => { + let source = skip_whitespace(&source[1..]); + let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?; + Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source)) + } + _ if is_identifier_char(next) => { + let len = source + .find(|c: char| !is_identifier_char(c)) + .unwrap_or(source.len()); + let (identifier, rest) = source.split_at(len); + source = skip_whitespace(rest); + Ok(( + KeyBindingContextPredicate::Identifier(identifier.to_string().into()), + source, + )) + } + _ => Err(anyhow!("unexpected character {next:?}")), + } + } + + fn new_or(self, other: Self) -> Result { + Ok(Self::Or(Box::new(self), Box::new(other))) + } + + fn new_and(self, other: Self) -> Result { + Ok(Self::And(Box::new(self), Box::new(other))) + } + + fn new_child(self, other: Self) -> Result { + Ok(Self::Child(Box::new(self), Box::new(other))) + } + + fn new_eq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::Equal(left, right)) + } else { + Err(anyhow!("operands must be identifiers")) + } + } + + fn new_neq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::NotEqual(left, right)) + } else { + Err(anyhow!("operands must be identifiers")) + } + } +} + +const PRECEDENCE_CHILD: u32 = 1; +const PRECEDENCE_OR: u32 = 2; +const PRECEDENCE_AND: u32 = 3; +const PRECEDENCE_EQ: u32 = 4; +const PRECEDENCE_NOT: u32 = 5; + +fn is_identifier_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' || c == '-' +} + +fn skip_whitespace(source: &str) -> &str { + let len = source + .find(|c: char| !c.is_whitespace()) + .unwrap_or(source.len()); + &source[len..] +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as gpui; + use KeyBindingContextPredicate::*; + + #[test] + fn test_actions_definition() { + { + actions!(A, B, C, D, E, F, G); + } + + { + actions!( + A, + B, + C, + D, + E, + F, + G, // Don't wrap, test the trailing comma + ); + } + } + + #[test] + fn test_parse_context() { + let mut expected = KeyBindingContext::default(); + expected.set("foo", "bar"); + expected.add("baz"); + assert_eq!(KeyBindingContext::parse("baz foo=bar").unwrap(), expected); + assert_eq!(KeyBindingContext::parse("foo = bar baz").unwrap(), expected); + assert_eq!( + KeyBindingContext::parse(" baz foo = bar baz").unwrap(), + expected + ); + assert_eq!( + KeyBindingContext::parse(" foo = bar baz").unwrap(), + expected + ); + } + + #[test] + fn test_parse_identifiers() { + // Identifiers + assert_eq!( + KeyBindingContextPredicate::parse("abc12").unwrap(), + Identifier("abc12".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("_1a").unwrap(), + Identifier("_1a".into()) + ); + } + + #[test] + fn test_parse_negations() { + assert_eq!( + KeyBindingContextPredicate::parse("!abc").unwrap(), + Not(Box::new(Identifier("abc".into()))) + ); + assert_eq!( + KeyBindingContextPredicate::parse(" ! ! abc").unwrap(), + Not(Box::new(Not(Box::new(Identifier("abc".into()))))) + ); + } + + #[test] + fn test_parse_equality_operators() { + assert_eq!( + KeyBindingContextPredicate::parse("a == b").unwrap(), + Equal("a".into(), "b".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("c!=d").unwrap(), + NotEqual("c".into(), "d".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("c == !d") + .unwrap_err() + .to_string(), + "operands must be identifiers" + ); + } + + #[test] + fn test_parse_boolean_operators() { + assert_eq!( + KeyBindingContextPredicate::parse("a || b").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a || !b && c").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(And( + Box::new(Not(Box::new(Identifier("b".into())))), + Box::new(Identifier("c".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(), + Or( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(And( + Box::new(Identifier("c".into())), + Box::new(Identifier("d".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(), + Or( + Box::new(And( + Box::new(Equal("a".into(), "b".into())), + Box::new(Identifier("c".into())) + )), + Box::new(And( + Box::new(Equal("d".into(), "e".into())), + Box::new(Identifier("f".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a && b && c && d").unwrap(), + And( + Box::new(And( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(Identifier("c".into())), + )), + Box::new(Identifier("d".into())) + ), + ); + } + + #[test] + fn test_parse_parenthesized_expressions() { + assert_eq!( + KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(), + And( + Box::new(Identifier("a".into())), + Box::new(Or( + Box::new(Equal("b".into(), "c".into())), + Box::new(NotEqual("d".into(), "e".into())), + )), + ), + ); + assert_eq!( + KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())), + ) + ); + } +} diff --git a/crates/gpui2/src/keymap/keymap.rs b/crates/gpui2/src/keymap/keymap.rs index eda493a460..989ee7a8d5 100644 --- a/crates/gpui2/src/keymap/keymap.rs +++ b/crates/gpui2/src/keymap/keymap.rs @@ -1,4 +1,4 @@ -use crate::{DispatchContextPredicate, KeyBinding, Keystroke}; +use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke}; use collections::HashSet; use smallvec::SmallVec; use std::{any::TypeId, collections::HashMap}; @@ -11,7 +11,7 @@ pub struct Keymap { bindings: Vec, binding_indices_by_action_id: HashMap>, disabled_keystrokes: - HashMap, HashSet>>, + HashMap, HashSet>>, version: KeymapVersion, } diff --git a/crates/gpui2/src/keymap/matcher.rs b/crates/gpui2/src/keymap/matcher.rs index c2033a9595..c9b5d26ecb 100644 --- a/crates/gpui2/src/keymap/matcher.rs +++ b/crates/gpui2/src/keymap/matcher.rs @@ -1,15 +1,15 @@ -use crate::{Action, DispatchContext, Keymap, KeymapVersion, Keystroke}; +use crate::{Action, KeyBindingContext, Keymap, KeymapVersion, Keystroke}; use parking_lot::Mutex; use smallvec::SmallVec; use std::sync::Arc; -pub struct KeyMatcher { +pub struct KeystrokeMatcher { pending_keystrokes: Vec, keymap: Arc>, keymap_version: KeymapVersion, } -impl KeyMatcher { +impl KeystrokeMatcher { pub fn new(keymap: Arc>) -> Self { let keymap_version = keymap.lock().version(); Self { @@ -44,7 +44,7 @@ impl KeyMatcher { pub fn match_keystroke( &mut self, keystroke: &Keystroke, - context_stack: &[&DispatchContext], + context_stack: &[KeyBindingContext], ) -> KeyMatch { let keymap = self.keymap.lock(); // Clear pending keystrokes if the keymap has changed since the last matched keystroke. @@ -86,7 +86,7 @@ impl KeyMatcher { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[&DispatchContext], + contexts: &[KeyBindingContext], ) -> Option> { self.keymap .lock() diff --git a/crates/gpui2/src/keymap/mod.rs b/crates/gpui2/src/keymap/mod.rs index 449b5427bf..09e222c095 100644 --- a/crates/gpui2/src/keymap/mod.rs +++ b/crates/gpui2/src/keymap/mod.rs @@ -1,7 +1,9 @@ mod binding; +mod context; mod keymap; mod matcher; pub use binding::*; +pub use context::*; pub use keymap::*; pub use matcher::*; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index fd3890d644..cde7b31754 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,15 +1,15 @@ use crate::{ build_action_from_type, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, - DevicePixels, DispatchContext, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, - FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, - IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, - MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, - PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, - PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, - SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, - TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, - WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + DevicePixels, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, + FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, + KeyBindingContext, KeyListener, KeyMatch, Keystroke, KeystrokeMatcher, LayoutId, Model, + ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, + PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, + RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, + Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, + VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -64,7 +64,7 @@ type AnyListener = Box Option> @@ -230,7 +230,7 @@ pub struct Window { #[derive(Default)] pub(crate) struct Frame { element_states: HashMap, - key_matchers: HashMap, + key_matchers: HashMap, mouse_listeners: HashMap>, pub(crate) focus_listeners: Vec, pub(crate) key_dispatch_stack: Vec, @@ -337,7 +337,7 @@ pub(crate) enum KeyDispatchStackFrame { event_type: TypeId, listener: AnyKeyListener, }, - Context(DispatchContext), + Context(KeyBindingContext), } /// Indicates which region of the window is visible. Content falling outside of this mask will not be @@ -1228,7 +1228,7 @@ impl<'a> WindowContext<'a> { } else if let Some(any_key_event) = event.keyboard_event() { let key_dispatch_stack = mem::take(&mut self.window.current_frame.key_dispatch_stack); let key_event_type = any_key_event.type_id(); - let mut context_stack = SmallVec::<[&DispatchContext; 16]>::new(); + let mut context_stack = SmallVec::<[&KeyBindingContext; 16]>::new(); for (ix, frame) in key_dispatch_stack.iter().enumerate() { match frame { @@ -1300,7 +1300,7 @@ impl<'a> WindowContext<'a> { &mut self, element_id: &GlobalElementId, keystroke: &Keystroke, - context_stack: &[&DispatchContext], + context_stack: &[KeyBindingContext], ) -> KeyMatch { let key_match = self .window @@ -1621,7 +1621,7 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { .previous_frame .key_matchers .remove(&global_id) - .unwrap_or_else(|| KeyMatcher::new(keymap)), + .unwrap_or_else(|| KeystrokeMatcher::new(keymap)), ); } @@ -2120,7 +2120,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { let handle = self.view().downgrade(); let listener = Box::new( move |event: &dyn Any, - context_stack: &[&DispatchContext], + context_stack: &[&KeyBindingContext], phase: DispatchPhase, cx: &mut WindowContext<'_>| { handle @@ -2154,7 +2154,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { pub fn with_key_dispatch_context( &mut self, - context: DispatchContext, + context: KeyBindingContext, f: impl FnOnce(&mut Self) -> R, ) -> R { if context.is_empty() { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5c678df317..1522b4ec4e 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -37,11 +37,11 @@ use futures::{ }; use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, - AsyncAppContext, AsyncWindowContext, Bounds, Component, DispatchContext, Div, Entity, EntityId, - EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, - Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription, - Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, - WindowOptions, + AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, + FocusHandle, GlobalPixels, KeyBindingContext, Model, ModelContext, ParentElement, Point, + Render, Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, + WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3743,8 +3743,8 @@ impl Render for Workspace { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let mut context = DispatchContext::default(); - context.insert("Workspace"); + let mut context = KeyBindingContext::default(); + context.add("Workspace"); cx.with_key_dispatch_context(context, |cx| { div() .relative() From 7eaba8fabc8f7e3d28459b713f352fb75a2663e3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 10 Nov 2023 14:47:45 -0700 Subject: [PATCH 011/126] WIP --- crates/editor2/src/editor.rs | 8 +- crates/editor2/src/element.rs | 19 +- crates/gpui2/src/dispatch.rs | 225 ---------- crates/gpui2/src/elements/div.rs | 95 +++-- crates/gpui2/src/elements/img.rs | 26 +- crates/gpui2/src/elements/svg.rs | 28 +- crates/gpui2/src/focusable.rs | 252 ------------ crates/gpui2/src/gpui2.rs | 6 +- crates/gpui2/src/interactive.rs | 44 +- crates/gpui2/src/key_dispatch.rs | 547 +++++++++++++++++++++++++ crates/gpui2/src/keymap/binding.rs | 8 +- crates/gpui2/src/keymap/context.rs | 21 +- crates/gpui2/src/keymap/matcher.rs | 6 +- crates/gpui2/src/prelude.rs | 1 + crates/gpui2/src/view.rs | 4 - crates/gpui2/src/window.rs | 451 ++++---------------- crates/picker2/src/picker2.rs | 8 +- crates/storybook2/src/stories/focus.rs | 4 +- crates/ui2/src/styled_ext.rs | 4 +- crates/workspace2/src/workspace2.rs | 302 +++++++------- 20 files changed, 908 insertions(+), 1151 deletions(-) delete mode 100644 crates/gpui2/src/dispatch.rs delete mode 100644 crates/gpui2/src/focusable.rs create mode 100644 crates/gpui2/src/key_dispatch.rs create mode 100644 crates/gpui2/src/prelude.rs diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ad17f96055..d432ae037c 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -42,7 +42,7 @@ use gpui::{ action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, - InputHandler, KeyBindingContext, Model, MouseButton, ParentElement, Pixels, Render, + InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -646,7 +646,7 @@ pub struct Editor { collapse_matches: bool, autoindent_mode: Option, workspace: Option<(WeakView, i64)>, - keymap_context_layers: BTreeMap, + keymap_context_layers: BTreeMap, input_enabled: bool, read_only: bool, leader_peer_id: Option, @@ -1980,8 +1980,8 @@ impl Editor { this } - fn dispatch_context(&self, cx: &AppContext) -> KeyBindingContext { - let mut dispatch_context = KeyBindingContext::default(); + fn dispatch_context(&self, cx: &AppContext) -> KeyContext { + let mut dispatch_context = KeyContext::default(); dispatch_context.add("Editor"); let mode = match self.mode { EditorMode::SingleLine => "single_line", diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 2cd319f66b..d42a14bb77 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -18,10 +18,9 @@ use gpui::{ black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, - KeyBindingContext, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, - ShapedGlyph, Size, Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, - WrappedLineLayout, + KeyContext, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size, + Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -2457,11 +2456,11 @@ impl Element for EditorElement { let dispatch_context = editor.dispatch_context(cx); cx.with_element_id(cx.view().entity_id(), |global_id, cx| { - cx.with_key_dispatch_context(dispatch_context, |cx| { - cx.with_key_listeners(build_key_listeners(global_id), |cx| { - cx.with_focus(editor.focus_handle.clone(), |_| {}) - }); - }) + cx.with_key_dispatch( + dispatch_context, + Some(editor.focus_handle.clone()), + |_, _| {}, + ) }); } @@ -4165,7 +4164,7 @@ fn build_key_listener( listener: impl Fn( &mut Editor, &T, - &[&KeyBindingContext], + &[&KeyContext], DispatchPhase, &mut ViewContext, ) -> Option> diff --git a/crates/gpui2/src/dispatch.rs b/crates/gpui2/src/dispatch.rs deleted file mode 100644 index 372c8c2610..0000000000 --- a/crates/gpui2/src/dispatch.rs +++ /dev/null @@ -1,225 +0,0 @@ -use crate::{ - Action, DispatchPhase, FocusId, KeyBindingContext, KeyDownEvent, KeyMatch, Keymap, - KeystrokeMatcher, WindowContext, -}; -use collections::HashMap; -use parking_lot::Mutex; -use smallvec::SmallVec; -use std::{any::Any, sync::Arc}; - -// trait KeyListener -> FnMut(&E, &mut V, &mut ViewContext) -type AnyKeyListener = Box; -type AnyActionListener = Box; - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub struct DispatchNodeId(usize); - -pub struct DispatchTree { - node_stack: Vec, - context_stack: Vec, - nodes: Vec, - focused: Option, - focusable_node_ids: HashMap, - keystroke_matchers: HashMap, KeystrokeMatcher>, - keymap: Arc>, -} - -#[derive(Default)] -pub struct DispatchNode { - key_listeners: SmallVec<[AnyKeyListener; 2]>, - action_listeners: SmallVec<[AnyActionListener; 16]>, - context: KeyBindingContext, - parent: Option, -} - -impl DispatchTree { - pub fn clear(&mut self) { - self.node_stack.clear(); - self.nodes.clear(); - } - - pub fn push_node(&mut self, context: Option, old_tree: &mut Self) { - let parent = self.node_stack.last().copied(); - let node_id = DispatchNodeId(self.nodes.len()); - self.nodes.push(DispatchNode { - parent, - ..Default::default() - }); - self.node_stack.push(node_id); - if let Some(context) = context { - self.context_stack.push(context); - if let Some((context_stack, matcher)) = old_tree - .keystroke_matchers - .remove_entry(self.context_stack.as_slice()) - { - self.keystroke_matchers.insert(context_stack, matcher); - } - } - } - - pub fn pop_node(&mut self) -> DispatchNodeId { - self.node_stack.pop().unwrap() - } - - pub fn on_key_event(&mut self, listener: AnyKeyListener) { - self.active_node().key_listeners.push(listener); - } - - pub fn on_action(&mut self, listener: AnyActionListener) { - self.active_node().action_listeners.push(listener); - } - - pub fn make_focusable(&mut self, focus_id: FocusId) { - self.focusable_node_ids - .insert(focus_id, self.active_node_id()); - } - - pub fn set_focus(&mut self, focus_id: Option) { - self.focused = focus_id; - } - - pub fn active_node(&mut self) -> &mut DispatchNode { - let node_id = self.active_node_id(); - &mut self.nodes[node_id.0] - } - - fn active_node_id(&self) -> DispatchNodeId { - *self.node_stack.last().unwrap() - } - - /// Returns the DispatchNodeIds from the root of the tree to the given target node id. - fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { - let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new(); - let mut current_node_id = Some(target); - while let Some(node_id) = current_node_id { - dispatch_path.push(node_id); - current_node_id = self.nodes[node_id.0].parent; - } - dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. - dispatch_path - } - - pub fn dispatch_key(&mut self, event: &dyn Any, cx: &mut WindowContext) { - if let Some(focused_node_id) = self - .focused - .and_then(|focus_id| self.focusable_node_ids.get(&focus_id)) - .copied() - { - self.dispatch_key_on_node(focused_node_id, event, cx); - } - } - - fn dispatch_key_on_node( - &mut self, - node_id: DispatchNodeId, - event: &dyn Any, - cx: &mut WindowContext, - ) { - let dispatch_path = self.dispatch_path(node_id); - - // Capture phase - self.context_stack.clear(); - cx.propagate_event = true; - for node_id in &dispatch_path { - let node = &self.nodes[node_id.0]; - if !node.context.is_empty() { - self.context_stack.push(node.context.clone()); - } - - for key_listener in &node.key_listeners { - key_listener(event, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; - } - } - } - - // Bubble phase - for node_id in dispatch_path.iter().rev() { - let node = &self.nodes[node_id.0]; - - // Handle low level key events - for key_listener in &node.key_listeners { - key_listener(event, DispatchPhase::Bubble, cx); - if !cx.propagate_event { - return; - } - } - - // Match keystrokes - if !node.context.is_empty() { - if let Some(key_down_event) = event.downcast_ref::() { - if !self - .keystroke_matchers - .contains_key(self.context_stack.as_slice()) - { - let keystroke_contexts = self.context_stack.iter().cloned().collect(); - self.keystroke_matchers.insert( - keystroke_contexts, - KeystrokeMatcher::new(self.keymap.clone()), - ); - } - - if let Some(keystroke_matcher) = self - .keystroke_matchers - .get_mut(self.context_stack.as_slice()) - { - if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke( - &key_down_event.keystroke, - self.context_stack.as_slice(), - ) { - self.dispatch_action_on_node(*node_id, action, cx); - if !cx.propagate_event { - return; - } - } - } - } - - self.context_stack.pop(); - } - } - } - - pub fn dispatch_action(&self, action: Box, cx: &mut WindowContext) { - if let Some(focused_node_id) = self - .focused - .and_then(|focus_id| self.focusable_node_ids.get(&focus_id)) - .copied() - { - self.dispatch_action_on_node(focused_node_id, action, cx); - } - } - - fn dispatch_action_on_node( - &self, - node_id: DispatchNodeId, - action: Box, - cx: &mut WindowContext, - ) { - let dispatch_path = self.dispatch_path(node_id); - - // Capture phase - for node_id in &dispatch_path { - let node = &self.nodes[node_id.0]; - for action_listener in &node.action_listeners { - action_listener(&action, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; - } - } - } - - // Bubble phase - for node_id in dispatch_path.iter().rev() { - let node = &self.nodes[node_id.0]; - for action_listener in &node.action_listeners { - cx.propagate_event = false; // Actions stop propagation by default during the bubble phase - action_listener(&action, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; - } - } - } - } -} diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index d88a4119b7..eaac9fc71e 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,29 +1,33 @@ +use std::fmt::Debug; + use crate::{ - point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementFocus, ElementId, - ElementInteractivity, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable, - GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement, - Pixels, Point, SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive, - StatelessInteractivity, Style, StyleRefinement, Styled, ViewContext, Visibility, + point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, ElementInteractivity, + FocusHandle, FocusListeners, Focusable, FocusableKeyDispatch, GlobalElementId, GroupBounds, + InteractiveElementState, KeyContext, KeyDispatch, LayoutId, NonFocusableKeyDispatch, Overflow, + ParentElement, Pixels, Point, SharedString, StatefulInteractive, StatefulInteractivity, + StatelessInteractive, StatelessInteractivity, Style, StyleRefinement, Styled, ViewContext, + Visibility, }; use refineable::Refineable; use smallvec::SmallVec; +use util::ResultExt; pub struct Div< V: 'static, I: ElementInteractivity = StatelessInteractivity, - F: ElementFocus = FocusDisabled, + K: KeyDispatch = NonFocusableKeyDispatch, > { interactivity: I, - focus: F, + key_dispatch: K, children: SmallVec<[AnyElement; 2]>, group: Option, base_style: StyleRefinement, } -pub fn div() -> Div, FocusDisabled> { +pub fn div() -> Div, NonFocusableKeyDispatch> { Div { interactivity: StatelessInteractivity::default(), - focus: FocusDisabled, + key_dispatch: NonFocusableKeyDispatch::default(), children: SmallVec::new(), group: None, base_style: StyleRefinement::default(), @@ -33,12 +37,12 @@ pub fn div() -> Div, FocusDisabled> { impl Div, F> where V: 'static, - F: ElementFocus, + F: KeyDispatch, { pub fn id(self, id: impl Into) -> Div, F> { Div { interactivity: StatefulInteractivity::new(id.into(), self.interactivity), - focus: self.focus, + key_dispatch: self.key_dispatch, children: self.children, group: self.group, base_style: self.base_style, @@ -49,7 +53,7 @@ where impl Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { pub fn group(mut self, group: impl Into) -> Self { self.group = Some(group.into()); @@ -61,6 +65,18 @@ where self } + pub fn context(mut self, context: C) -> Self + where + Self: Sized, + C: TryInto, + C::Error: Debug, + { + if let Some(context) = context.try_into().log_err() { + *self.key_dispatch.key_context_mut() = context; + } + self + } + pub fn overflow_hidden(mut self) -> Self { self.base_style.overflow.x = Some(Overflow::Hidden); self.base_style.overflow.y = Some(Overflow::Hidden); @@ -97,7 +113,7 @@ where ) -> Style { let mut computed_style = Style::default(); computed_style.refine(&self.base_style); - self.focus.refine_style(&mut computed_style, cx); + self.key_dispatch.refine_style(&mut computed_style, cx); self.interactivity.refine_style( &mut computed_style, bounds, @@ -108,11 +124,11 @@ where } } -impl Div, FocusDisabled> { - pub fn focusable(self) -> Div, FocusEnabled> { +impl Div, NonFocusableKeyDispatch> { + pub fn focusable(self) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity, - focus: FocusEnabled::new(), + key_dispatch: FocusableKeyDispatch::new(), children: self.children, group: self.group, base_style: self.base_style, @@ -122,10 +138,10 @@ impl Div, FocusDisabled> { pub fn track_focus( self, handle: &FocusHandle, - ) -> Div, FocusEnabled> { + ) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity, - focus: FocusEnabled::tracked(handle), + key_dispatch: FocusableKeyDispatch::tracked(handle), children: self.children, group: self.group, base_style: self.base_style, @@ -149,14 +165,14 @@ impl Div, FocusDisabled> { } } -impl Div, FocusDisabled> { +impl Div, NonFocusableKeyDispatch> { pub fn track_focus( self, handle: &FocusHandle, - ) -> Div, FocusEnabled> { + ) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity.into_stateful(handle), - focus: handle.clone().into(), + key_dispatch: handle.clone().into(), children: self.children, group: self.group, base_style: self.base_style, @@ -164,25 +180,25 @@ impl Div, FocusDisabled> { } } -impl Focusable for Div> +impl Focusable for Div> where V: 'static, I: ElementInteractivity, { fn focus_listeners(&mut self) -> &mut FocusListeners { - &mut self.focus.focus_listeners + &mut self.key_dispatch.focus_listeners } fn set_focus_style(&mut self, style: StyleRefinement) { - self.focus.focus_style = style; + self.key_dispatch.focus_style = style; } fn set_focus_in_style(&mut self, style: StyleRefinement) { - self.focus.focus_in_style = style; + self.key_dispatch.focus_in_style = style; } fn set_in_focus_style(&mut self, style: StyleRefinement) { - self.focus.in_focus_style = style; + self.key_dispatch.in_focus_style = style; } } @@ -196,7 +212,7 @@ pub struct DivState { impl Element for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { type ElementState = DivState; @@ -213,14 +229,17 @@ where cx: &mut ViewContext, ) -> Self::ElementState { let mut element_state = element_state.unwrap_or_default(); - self.interactivity.initialize(cx, |cx| { - self.focus - .initialize(element_state.focus_handle.take(), cx, |focus_handle, cx| { + self.with_element_id(cx, |this, _global_id, cx| { + this.key_dispatch.initialize( + element_state.focus_handle.take(), + cx, + |focus_handle, cx| { element_state.focus_handle = focus_handle; - for child in &mut self.children { + for child in &mut this.children { child.initialize(view_state, cx); } - }) + }, + ); }); element_state } @@ -288,7 +307,7 @@ where cx.with_z_index(z_index, |cx| { cx.with_z_index(0, |cx| { style.paint(bounds, cx); - this.focus.paint(bounds, cx); + this.key_dispatch.paint(bounds, cx); this.interactivity.paint( bounds, content_size, @@ -321,7 +340,7 @@ where impl Component for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn render(self) -> AnyElement { AnyElement::new(self) @@ -331,7 +350,7 @@ where impl ParentElement for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children @@ -341,7 +360,7 @@ where impl Styled for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn style(&mut self) -> &mut StyleRefinement { &mut self.base_style @@ -351,7 +370,7 @@ where impl StatelessInteractive for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { self.interactivity.as_stateless_mut() @@ -360,7 +379,7 @@ where impl StatefulInteractive for Div, F> where - F: ElementFocus, + F: KeyDispatch, { fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { &mut self.interactivity diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 638665d414..1ff088c1af 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,7 +1,7 @@ use crate::{ - div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementFocus, - ElementId, ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, - LayoutId, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, + div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementId, + ElementInteractivity, FocusListeners, Focusable, FocusableKeyDispatch, KeyDispatch, LayoutId, + NonFocusableKeyDispatch, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, }; use futures::FutureExt; @@ -10,14 +10,14 @@ use util::ResultExt; pub struct Img< V: 'static, I: ElementInteractivity = StatelessInteractivity, - F: ElementFocus = FocusDisabled, + F: KeyDispatch = NonFocusableKeyDispatch, > { base: Div, uri: Option, grayscale: bool, } -pub fn img() -> Img, FocusDisabled> { +pub fn img() -> Img, NonFocusableKeyDispatch> { Img { base: div(), uri: None, @@ -29,7 +29,7 @@ impl Img where V: 'static, I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { pub fn uri(mut self, uri: impl Into) -> Self { self.uri = Some(uri.into()); @@ -44,7 +44,7 @@ where impl Img, F> where - F: ElementFocus, + F: KeyDispatch, { pub fn id(self, id: impl Into) -> Img, F> { Img { @@ -58,7 +58,7 @@ where impl Component for Img where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn render(self) -> AnyElement { AnyElement::new(self) @@ -68,7 +68,7 @@ where impl Element for Img where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { type ElementState = DivState; @@ -137,7 +137,7 @@ where impl Styled for Img where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn style(&mut self) -> &mut StyleRefinement { self.base.style() @@ -147,7 +147,7 @@ where impl StatelessInteractive for Img where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { self.base.stateless_interactivity() @@ -156,14 +156,14 @@ where impl StatefulInteractive for Img, F> where - F: ElementFocus, + F: KeyDispatch, { fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { self.base.stateful_interactivity() } } -impl Focusable for Img> +impl Focusable for Img> where V: 'static, I: ElementInteractivity, diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index 8e2ba9d8a1..bafedb7f2d 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,21 +1,21 @@ use crate::{ - div, AnyElement, Bounds, Component, Div, DivState, Element, ElementFocus, ElementId, - ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels, - SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive, - StatelessInteractivity, StyleRefinement, Styled, ViewContext, + div, AnyElement, Bounds, Component, Div, DivState, Element, ElementId, ElementInteractivity, + FocusListeners, Focusable, FocusableKeyDispatch, KeyDispatch, LayoutId, + NonFocusableKeyDispatch, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, + StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, }; use util::ResultExt; pub struct Svg< V: 'static, I: ElementInteractivity = StatelessInteractivity, - F: ElementFocus = FocusDisabled, + F: KeyDispatch = NonFocusableKeyDispatch, > { base: Div, path: Option, } -pub fn svg() -> Svg, FocusDisabled> { +pub fn svg() -> Svg, NonFocusableKeyDispatch> { Svg { base: div(), path: None, @@ -25,7 +25,7 @@ pub fn svg() -> Svg, FocusDisabled> { impl Svg where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { pub fn path(mut self, path: impl Into) -> Self { self.path = Some(path.into()); @@ -35,7 +35,7 @@ where impl Svg, F> where - F: ElementFocus, + F: KeyDispatch, { pub fn id(self, id: impl Into) -> Svg, F> { Svg { @@ -48,7 +48,7 @@ where impl Component for Svg where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn render(self) -> AnyElement { AnyElement::new(self) @@ -58,7 +58,7 @@ where impl Element for Svg where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { type ElementState = DivState; @@ -108,7 +108,7 @@ where impl Styled for Svg where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn style(&mut self) -> &mut StyleRefinement { self.base.style() @@ -118,7 +118,7 @@ where impl StatelessInteractive for Svg where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { self.base.stateless_interactivity() @@ -128,14 +128,14 @@ where impl StatefulInteractive for Svg, F> where V: 'static, - F: ElementFocus, + F: KeyDispatch, { fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { self.base.stateful_interactivity() } } -impl Focusable for Svg> +impl Focusable for Svg> where I: ElementInteractivity, { diff --git a/crates/gpui2/src/focusable.rs b/crates/gpui2/src/focusable.rs deleted file mode 100644 index 99f8bb1dd6..0000000000 --- a/crates/gpui2/src/focusable.rs +++ /dev/null @@ -1,252 +0,0 @@ -use crate::{ - Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, MouseDownEvent, Pixels, Style, - StyleRefinement, ViewContext, WindowContext, -}; -use refineable::Refineable; -use smallvec::SmallVec; - -pub type FocusListeners = SmallVec<[FocusListener; 2]>; - -pub type FocusListener = - Box) + 'static>; - -pub trait Focusable: Element { - fn focus_listeners(&mut self) -> &mut FocusListeners; - fn set_focus_style(&mut self, style: StyleRefinement); - fn set_focus_in_style(&mut self, style: StyleRefinement); - fn set_in_focus_style(&mut self, style: StyleRefinement); - - fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.set_focus_style(f(StyleRefinement::default())); - self - } - - fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.set_focus_in_style(f(StyleRefinement::default())); - self - } - - fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.set_in_focus_style(f(StyleRefinement::default())); - self - } - - fn on_focus( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - if event.focused.as_ref() == Some(focus_handle) { - listener(view, event, cx) - } - })); - self - } - - fn on_blur( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - if event.blurred.as_ref() == Some(focus_handle) { - listener(view, event, cx) - } - })); - self - } - - fn on_focus_in( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - let descendant_blurred = event - .blurred - .as_ref() - .map_or(false, |blurred| focus_handle.contains(blurred, cx)); - let descendant_focused = event - .focused - .as_ref() - .map_or(false, |focused| focus_handle.contains(focused, cx)); - - if !descendant_blurred && descendant_focused { - listener(view, event, cx) - } - })); - self - } - - fn on_focus_out( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - let descendant_blurred = event - .blurred - .as_ref() - .map_or(false, |blurred| focus_handle.contains(blurred, cx)); - let descendant_focused = event - .focused - .as_ref() - .map_or(false, |focused| focus_handle.contains(focused, cx)); - if descendant_blurred && !descendant_focused { - listener(view, event, cx) - } - })); - self - } -} - -pub trait ElementFocus: 'static { - fn as_focusable(&self) -> Option<&FocusEnabled>; - fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled>; - - fn initialize( - &mut self, - focus_handle: Option, - cx: &mut ViewContext, - f: impl FnOnce(Option, &mut ViewContext) -> R, - ) -> R { - if let Some(focusable) = self.as_focusable_mut() { - let focus_handle = focusable - .focus_handle - .get_or_insert_with(|| focus_handle.unwrap_or_else(|| cx.focus_handle())) - .clone(); - for listener in focusable.focus_listeners.drain(..) { - let focus_handle = focus_handle.clone(); - cx.on_focus_changed(move |view, event, cx| { - listener(view, &focus_handle, event, cx) - }); - } - cx.with_focus(focus_handle.clone(), |cx| f(Some(focus_handle), cx)) - } else { - f(None, cx) - } - } - - fn refine_style(&self, style: &mut Style, cx: &WindowContext) { - if let Some(focusable) = self.as_focusable() { - let focus_handle = focusable - .focus_handle - .as_ref() - .expect("must call initialize before refine_style"); - if focus_handle.contains_focused(cx) { - style.refine(&focusable.focus_in_style); - } - - if focus_handle.within_focused(cx) { - style.refine(&focusable.in_focus_style); - } - - if focus_handle.is_focused(cx) { - style.refine(&focusable.focus_style); - } - } - } - - fn paint(&self, bounds: Bounds, cx: &mut WindowContext) { - if let Some(focusable) = self.as_focusable() { - let focus_handle = focusable - .focus_handle - .clone() - .expect("must call initialize before paint"); - cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - if !cx.default_prevented() { - cx.focus(&focus_handle); - cx.prevent_default(); - } - } - }) - } - } -} - -pub struct FocusEnabled { - pub focus_handle: Option, - pub focus_listeners: FocusListeners, - pub focus_style: StyleRefinement, - pub focus_in_style: StyleRefinement, - pub in_focus_style: StyleRefinement, -} - -impl FocusEnabled { - pub fn new() -> Self { - Self { - focus_handle: None, - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } - } - - pub fn tracked(handle: &FocusHandle) -> Self { - Self { - focus_handle: Some(handle.clone()), - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } - } -} - -impl ElementFocus for FocusEnabled { - fn as_focusable(&self) -> Option<&FocusEnabled> { - Some(self) - } - - fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled> { - Some(self) - } -} - -impl From for FocusEnabled { - fn from(value: FocusHandle) -> Self { - Self { - focus_handle: Some(value), - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } - } -} - -pub struct FocusDisabled; - -impl ElementFocus for FocusDisabled { - fn as_focusable(&self) -> Option<&FocusEnabled> { - None - } - - fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled> { - None - } -} diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 42aea446f1..87de7998a8 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -3,17 +3,17 @@ mod action; mod app; mod assets; mod color; -mod dispatch; mod element; mod elements; mod executor; -mod focusable; mod geometry; mod image_cache; mod input; mod interactive; +mod key_dispatch; mod keymap; mod platform; +pub mod prelude; mod scene; mod style; mod styled; @@ -42,12 +42,12 @@ pub use ctor::ctor; pub use element::*; pub use elements::*; pub use executor::*; -pub use focusable::*; pub use geometry::*; pub use gpui2_macros::*; pub use image_cache::*; pub use input::*; pub use interactive::*; +pub use key_dispatch::*; pub use keymap::*; pub use platform::*; use private::Sealed; diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 946a59a809..9ac1f56099 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,8 +1,8 @@ use crate::{ - div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, BorrowWindow, Bounds, - Component, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyBindingContext, KeyMatch, - Keystroke, Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style, - StyleRefinement, Task, View, ViewContext, + div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, Bounds, Component, + DispatchPhase, Div, Element, ElementId, FocusHandle, KeyContext, Keystroke, Modifiers, + Overflow, Pixels, Point, Render, SharedString, Size, Style, StyleRefinement, Task, View, + ViewContext, }; use collections::HashMap; use derive_more::{Deref, DerefMut}; @@ -164,17 +164,6 @@ pub trait StatelessInteractive: Element { self } - fn context(mut self, context: C) -> Self - where - Self: Sized, - C: TryInto, - C::Error: Debug, - { - self.stateless_interactivity().dispatch_context = - context.try_into().expect("invalid dispatch context"); - self - } - /// Capture the given action, fires during the capture phase fn capture_action( mut self, @@ -396,25 +385,6 @@ pub trait ElementInteractivity: 'static { fn as_stateful(&self) -> Option<&StatefulInteractivity>; fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity>; - fn initialize( - &mut self, - cx: &mut ViewContext, - f: impl FnOnce(&mut ViewContext) -> R, - ) -> R { - if let Some(stateful) = self.as_stateful_mut() { - cx.with_element_id(stateful.id.clone(), |global_id, cx| { - cx.with_key_dispatch_context(stateful.dispatch_context.clone(), |cx| { - cx.with_key_listeners(mem::take(&mut stateful.key_listeners), f) - }) - }) - } else { - let stateless = self.as_stateless_mut(); - cx.with_key_dispatch_context(stateless.dispatch_context.clone(), |cx| { - cx.with_key_listeners(mem::take(&mut stateless.key_listeners), f) - }) - } - } - fn refine_style( &self, style: &mut Style, @@ -790,7 +760,7 @@ impl ElementInteractivity for StatefulInteractivity { type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; pub struct StatelessInteractivity { - pub dispatch_context: KeyBindingContext, + pub dispatch_context: KeyContext, pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, @@ -892,7 +862,7 @@ impl InteractiveElementState { impl Default for StatelessInteractivity { fn default() -> Self { Self { - dispatch_context: KeyBindingContext::default(), + dispatch_context: KeyContext::default(), mouse_down_listeners: SmallVec::new(), mouse_up_listeners: SmallVec::new(), mouse_move_listeners: SmallVec::new(), @@ -1236,7 +1206,7 @@ pub type KeyListener = Box< dyn Fn( &mut V, &dyn Any, - &[&KeyBindingContext], + &[&KeyContext], DispatchPhase, &mut ViewContext, ) -> Option> diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs new file mode 100644 index 0000000000..9f76df82c3 --- /dev/null +++ b/crates/gpui2/src/key_dispatch.rs @@ -0,0 +1,547 @@ +use crate::{ + build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, + FocusId, KeyContext, KeyDownEvent, KeyMatch, Keymap, KeystrokeMatcher, MouseDownEvent, Pixels, + Style, StyleRefinement, ViewContext, WindowContext, +}; +use collections::HashMap; +use parking_lot::Mutex; +use refineable::Refineable; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; +use util::ResultExt; + +type KeyListener = Box; +pub type FocusListeners = SmallVec<[FocusListener; 2]>; +pub type FocusListener = + Box) + 'static>; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct DispatchNodeId(usize); + +pub struct KeyDispatcher { + node_stack: Vec, + context_stack: Vec, + nodes: Vec, + focusable_node_ids: HashMap, + keystroke_matchers: HashMap, KeystrokeMatcher>, + keymap: Arc>, +} + +#[derive(Default)] +pub struct DispatchNode { + key_listeners: SmallVec<[KeyListener; 2]>, + action_listeners: SmallVec<[ActionListener; 16]>, + context: KeyContext, + parent: Option, +} + +struct ActionListener { + action_type: TypeId, + listener: Box, +} + +impl KeyDispatcher { + pub fn new(keymap: Arc>) -> Self { + Self { + node_stack: Vec::new(), + context_stack: Vec::new(), + nodes: Vec::new(), + focusable_node_ids: HashMap::default(), + keystroke_matchers: HashMap::default(), + keymap, + } + } + + pub fn clear(&mut self) { + self.node_stack.clear(); + self.nodes.clear(); + } + + pub fn push_node(&mut self, context: KeyContext, old_dispatcher: &mut Self) { + let parent = self.node_stack.last().copied(); + let node_id = DispatchNodeId(self.nodes.len()); + self.nodes.push(DispatchNode { + parent, + ..Default::default() + }); + self.node_stack.push(node_id); + if !context.is_empty() { + self.context_stack.push(context); + if let Some((context_stack, matcher)) = old_dispatcher + .keystroke_matchers + .remove_entry(self.context_stack.as_slice()) + { + self.keystroke_matchers.insert(context_stack, matcher); + } + } + } + + pub fn pop_node(&mut self) { + let node_id = self.node_stack.pop().unwrap(); + if !self.nodes[node_id.0].context.is_empty() { + self.context_stack.pop(); + } + } + + pub fn on_key_event(&mut self, listener: KeyListener) { + self.active_node().key_listeners.push(listener); + } + + pub fn on_action( + &mut self, + action_type: TypeId, + listener: Box, + ) { + self.active_node().action_listeners.push(ActionListener { + action_type, + listener, + }); + } + + pub fn make_focusable(&mut self, focus_id: FocusId) { + self.focusable_node_ids + .insert(focus_id, self.active_node_id()); + } + + pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool { + if parent == child { + return true; + } + + if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) { + let mut current_node_id = self.focusable_node_ids.get(&child).copied(); + while let Some(node_id) = current_node_id { + if node_id == *parent_node_id { + return true; + } + current_node_id = self.nodes[node_id.0].parent; + } + } + false + } + + pub fn available_actions(&self, target: FocusId) -> Vec> { + let mut actions = Vec::new(); + if let Some(node) = self.focusable_node_ids.get(&target) { + for node_id in self.dispatch_path(*node) { + let node = &self.nodes[node_id.0]; + for ActionListener { action_type, .. } in &node.action_listeners { + actions.extend(build_action_from_type(action_type).log_err()); + } + } + } + actions + } + + pub fn dispatch_key(&mut self, target: FocusId, event: &dyn Any, cx: &mut WindowContext) { + if let Some(target_node_id) = self.focusable_node_ids.get(&target).copied() { + self.dispatch_key_on_node(target_node_id, event, cx); + } + } + + fn dispatch_key_on_node( + &mut self, + node_id: DispatchNodeId, + event: &dyn Any, + cx: &mut WindowContext, + ) { + let dispatch_path = self.dispatch_path(node_id); + + // Capture phase + self.context_stack.clear(); + cx.propagate_event = true; + for node_id in &dispatch_path { + let node = &self.nodes[node_id.0]; + if !node.context.is_empty() { + self.context_stack.push(node.context.clone()); + } + + for key_listener in &node.key_listeners { + key_listener(event, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = &self.nodes[node_id.0]; + + // Handle low level key events + for key_listener in &node.key_listeners { + key_listener(event, DispatchPhase::Bubble, cx); + if !cx.propagate_event { + return; + } + } + + // Match keystrokes + if !node.context.is_empty() { + if let Some(key_down_event) = event.downcast_ref::() { + if !self + .keystroke_matchers + .contains_key(self.context_stack.as_slice()) + { + let keystroke_contexts = self.context_stack.iter().cloned().collect(); + self.keystroke_matchers.insert( + keystroke_contexts, + KeystrokeMatcher::new(self.keymap.clone()), + ); + } + + if let Some(keystroke_matcher) = self + .keystroke_matchers + .get_mut(self.context_stack.as_slice()) + { + if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke( + &key_down_event.keystroke, + self.context_stack.as_slice(), + ) { + self.dispatch_action_on_node(*node_id, action, cx); + if !cx.propagate_event { + return; + } + } + } + } + + self.context_stack.pop(); + } + } + } + + pub fn dispatch_action( + &self, + target: FocusId, + action: Box, + cx: &mut WindowContext, + ) { + if let Some(target_node_id) = self.focusable_node_ids.get(&target).copied() { + self.dispatch_action_on_node(target_node_id, action, cx); + } + } + + fn dispatch_action_on_node( + &self, + node_id: DispatchNodeId, + action: Box, + cx: &mut WindowContext, + ) { + let dispatch_path = self.dispatch_path(node_id); + + // Capture phase + for node_id in &dispatch_path { + let node = &self.nodes[node_id.0]; + for ActionListener { listener, .. } in &node.action_listeners { + listener(&action, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = &self.nodes[node_id.0]; + for ActionListener { listener, .. } in &node.action_listeners { + cx.propagate_event = false; // Actions stop propagation by default during the bubble phase + listener(&action, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + } + + fn active_node(&mut self) -> &mut DispatchNode { + let active_node_id = self.active_node_id(); + &mut self.nodes[active_node_id.0] + } + + fn active_node_id(&self) -> DispatchNodeId { + *self.node_stack.last().unwrap() + } + + /// Returns the DispatchNodeIds from the root of the tree to the given target node id. + fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { + let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new(); + let mut current_node_id = Some(target); + while let Some(node_id) = current_node_id { + dispatch_path.push(node_id); + current_node_id = self.nodes[node_id.0].parent; + } + dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. + dispatch_path + } +} + +pub trait KeyDispatch: 'static { + fn as_focusable(&self) -> Option<&FocusableKeyDispatch>; + fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch>; + fn key_context(&self) -> &KeyContext; + fn key_context_mut(&mut self) -> &mut KeyContext; + + fn initialize( + &mut self, + focus_handle: Option, + cx: &mut ViewContext, + f: impl FnOnce(Option, &mut ViewContext) -> R, + ) -> R { + if let Some(focusable) = self.as_focusable_mut() { + let focus_handle = focusable + .focus_handle + .get_or_insert_with(|| focus_handle.unwrap_or_else(|| cx.focus_handle())) + .clone(); + for listener in focusable.focus_listeners.drain(..) { + let focus_handle = focus_handle.clone(); + cx.on_focus_changed(move |view, event, cx| { + listener(view, &focus_handle, event, cx) + }); + } + + cx.with_key_dispatch(self.key_context().clone(), Some(focus_handle), f) + } else { + f(None, cx) + } + } + + fn refine_style(&self, style: &mut Style, cx: &WindowContext) { + if let Some(focusable) = self.as_focusable() { + let focus_handle = focusable + .focus_handle + .as_ref() + .expect("must call initialize before refine_style"); + if focus_handle.contains_focused(cx) { + style.refine(&focusable.focus_in_style); + } + + if focus_handle.within_focused(cx) { + style.refine(&focusable.in_focus_style); + } + + if focus_handle.is_focused(cx) { + style.refine(&focusable.focus_style); + } + } + } + + fn paint(&self, bounds: Bounds, cx: &mut WindowContext) { + if let Some(focusable) = self.as_focusable() { + let focus_handle = focusable + .focus_handle + .clone() + .expect("must call initialize before paint"); + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if !cx.default_prevented() { + cx.focus(&focus_handle); + cx.prevent_default(); + } + } + }) + } + } +} + +pub struct FocusableKeyDispatch { + pub key_context: KeyContext, + pub focus_handle: Option, + pub focus_listeners: FocusListeners, + pub focus_style: StyleRefinement, + pub focus_in_style: StyleRefinement, + pub in_focus_style: StyleRefinement, +} + +impl FocusableKeyDispatch { + pub fn new() -> Self { + Self { + key_context: KeyContext::default(), + focus_handle: None, + focus_listeners: FocusListeners::default(), + focus_style: StyleRefinement::default(), + focus_in_style: StyleRefinement::default(), + in_focus_style: StyleRefinement::default(), + } + } + + pub fn tracked(handle: &FocusHandle) -> Self { + Self { + key_context: KeyContext::default(), + focus_handle: Some(handle.clone()), + focus_listeners: FocusListeners::default(), + focus_style: StyleRefinement::default(), + focus_in_style: StyleRefinement::default(), + in_focus_style: StyleRefinement::default(), + } + } +} + +impl KeyDispatch for FocusableKeyDispatch { + fn as_focusable(&self) -> Option<&FocusableKeyDispatch> { + Some(self) + } + + fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch> { + Some(self) + } + + fn key_context(&self) -> &KeyContext { + &self.key_context + } + + fn key_context_mut(&mut self) -> &mut KeyContext { + &mut self.key_context + } +} + +impl From for FocusableKeyDispatch { + fn from(value: FocusHandle) -> Self { + Self { + key_context: KeyContext::default(), + focus_handle: Some(value), + focus_listeners: FocusListeners::default(), + focus_style: StyleRefinement::default(), + focus_in_style: StyleRefinement::default(), + in_focus_style: StyleRefinement::default(), + } + } +} + +#[derive(Default)] +pub struct NonFocusableKeyDispatch { + pub(crate) key_context: KeyContext, +} + +impl KeyDispatch for NonFocusableKeyDispatch { + fn as_focusable(&self) -> Option<&FocusableKeyDispatch> { + None + } + + fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch> { + None + } + + fn key_context(&self) -> &KeyContext { + &self.key_context + } + + fn key_context_mut(&mut self) -> &mut KeyContext { + &mut self.key_context + } +} + +pub trait Focusable: Element { + fn focus_listeners(&mut self) -> &mut FocusListeners; + fn set_focus_style(&mut self, style: StyleRefinement); + fn set_focus_in_style(&mut self, style: StyleRefinement); + fn set_in_focus_style(&mut self, style: StyleRefinement); + + fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.set_focus_style(f(StyleRefinement::default())); + self + } + + fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.set_focus_in_style(f(StyleRefinement::default())); + self + } + + fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.set_in_focus_style(f(StyleRefinement::default())); + self + } + + fn on_focus( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.focus_listeners() + .push(Box::new(move |view, focus_handle, event, cx| { + if event.focused.as_ref() == Some(focus_handle) { + listener(view, event, cx) + } + })); + self + } + + fn on_blur( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.focus_listeners() + .push(Box::new(move |view, focus_handle, event, cx| { + if event.blurred.as_ref() == Some(focus_handle) { + listener(view, event, cx) + } + })); + self + } + + fn on_focus_in( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.focus_listeners() + .push(Box::new(move |view, focus_handle, event, cx| { + let descendant_blurred = event + .blurred + .as_ref() + .map_or(false, |blurred| focus_handle.contains(blurred, cx)); + let descendant_focused = event + .focused + .as_ref() + .map_or(false, |focused| focus_handle.contains(focused, cx)); + + if !descendant_blurred && descendant_focused { + listener(view, event, cx) + } + })); + self + } + + fn on_focus_out( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.focus_listeners() + .push(Box::new(move |view, focus_handle, event, cx| { + let descendant_blurred = event + .blurred + .as_ref() + .map_or(false, |blurred| focus_handle.contains(blurred, cx)); + let descendant_focused = event + .focused + .as_ref() + .map_or(false, |focused| focus_handle.contains(focused, cx)); + if descendant_blurred && !descendant_focused { + listener(view, event, cx) + } + })); + self + } +} diff --git a/crates/gpui2/src/keymap/binding.rs b/crates/gpui2/src/keymap/binding.rs index 1cf62484b9..9fbd0018b9 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -1,4 +1,4 @@ -use crate::{Action, KeyBindingContext, KeyBindingContextPredicate, KeyMatch, Keystroke}; +use crate::{Action, KeyBindingContextPredicate, KeyContext, KeyMatch, Keystroke}; use anyhow::Result; use smallvec::SmallVec; @@ -32,7 +32,7 @@ impl KeyBinding { }) } - pub fn matches_context(&self, contexts: &[KeyBindingContext]) -> bool { + pub fn matches_context(&self, contexts: &[KeyContext]) -> bool { self.context_predicate .as_ref() .map(|predicate| predicate.eval(contexts)) @@ -42,7 +42,7 @@ impl KeyBinding { pub fn match_keystrokes( &self, pending_keystrokes: &[Keystroke], - contexts: &[KeyBindingContext], + contexts: &[KeyContext], ) -> KeyMatch { if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.matches_context(contexts) @@ -61,7 +61,7 @@ impl KeyBinding { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[KeyBindingContext], + contexts: &[KeyContext], ) -> Option> { if self.action.partial_eq(action) && self.matches_context(contexts) { Some(self.keystrokes.clone()) diff --git a/crates/gpui2/src/keymap/context.rs b/crates/gpui2/src/keymap/context.rs index 834bd4989a..b0225e73e7 100644 --- a/crates/gpui2/src/keymap/context.rs +++ b/crates/gpui2/src/keymap/context.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, Result}; use smallvec::SmallVec; #[derive(Clone, Debug, Default, Eq, PartialEq, Hash)] -pub struct KeyBindingContext(SmallVec<[ContextEntry; 8]>); +pub struct KeyContext(SmallVec<[ContextEntry; 8]>); #[derive(Clone, Debug, Eq, PartialEq, Hash)] struct ContextEntry { @@ -11,7 +11,7 @@ struct ContextEntry { value: Option, } -impl<'a> TryFrom<&'a str> for KeyBindingContext { +impl<'a> TryFrom<&'a str> for KeyContext { type Error = anyhow::Error; fn try_from(value: &'a str) -> Result { @@ -19,7 +19,7 @@ impl<'a> TryFrom<&'a str> for KeyBindingContext { } } -impl KeyBindingContext { +impl KeyContext { pub fn parse(source: &str) -> Result { let mut context = Self::default(); let source = skip_whitespace(source); @@ -130,7 +130,7 @@ impl KeyBindingContextPredicate { } } - pub fn eval(&self, contexts: &[KeyBindingContext]) -> bool { + pub fn eval(&self, contexts: &[KeyContext]) -> bool { let Some(context) = contexts.last() else { return false; }; @@ -293,19 +293,16 @@ mod tests { #[test] fn test_parse_context() { - let mut expected = KeyBindingContext::default(); + let mut expected = KeyContext::default(); expected.set("foo", "bar"); expected.add("baz"); - assert_eq!(KeyBindingContext::parse("baz foo=bar").unwrap(), expected); - assert_eq!(KeyBindingContext::parse("foo = bar baz").unwrap(), expected); + assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected); + assert_eq!(KeyContext::parse("foo = bar baz").unwrap(), expected); assert_eq!( - KeyBindingContext::parse(" baz foo = bar baz").unwrap(), - expected - ); - assert_eq!( - KeyBindingContext::parse(" foo = bar baz").unwrap(), + KeyContext::parse(" baz foo = bar baz").unwrap(), expected ); + assert_eq!(KeyContext::parse(" foo = bar baz").unwrap(), expected); } #[test] diff --git a/crates/gpui2/src/keymap/matcher.rs b/crates/gpui2/src/keymap/matcher.rs index c9b5d26ecb..bab9c0f575 100644 --- a/crates/gpui2/src/keymap/matcher.rs +++ b/crates/gpui2/src/keymap/matcher.rs @@ -1,4 +1,4 @@ -use crate::{Action, KeyBindingContext, Keymap, KeymapVersion, Keystroke}; +use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke}; use parking_lot::Mutex; use smallvec::SmallVec; use std::sync::Arc; @@ -44,7 +44,7 @@ impl KeystrokeMatcher { pub fn match_keystroke( &mut self, keystroke: &Keystroke, - context_stack: &[KeyBindingContext], + context_stack: &[KeyContext], ) -> KeyMatch { let keymap = self.keymap.lock(); // Clear pending keystrokes if the keymap has changed since the last matched keystroke. @@ -86,7 +86,7 @@ impl KeystrokeMatcher { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[KeyBindingContext], + contexts: &[KeyContext], ) -> Option> { self.keymap .lock() diff --git a/crates/gpui2/src/prelude.rs b/crates/gpui2/src/prelude.rs new file mode 100644 index 0000000000..bc998fc1f4 --- /dev/null +++ b/crates/gpui2/src/prelude.rs @@ -0,0 +1 @@ +pub use crate::{Context, ParentElement, Refineable}; diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 00e1e55cd5..d12d84f43b 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -184,10 +184,6 @@ impl AnyView { .compute_layout(layout_id, available_space); (self.paint)(self, &mut rendered_element, cx); } - - pub(crate) fn draw_dispatch_stack(&self, cx: &mut WindowContext) { - (self.initialize)(self, cx); - } } impl Component for AnyView { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index cde7b31754..15a59253e7 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,15 +1,14 @@ use crate::{ - build_action_from_type, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, - AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, - DevicePixels, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, - FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, - KeyBindingContext, KeyListener, KeyMatch, Keystroke, KeystrokeMatcher, LayoutId, Model, - ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, - PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, - RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, - Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, - VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, + Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DisplayId, Edges, Effect, + Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, + Hsla, ImageData, InputEvent, IsZero, KeyContext, KeyDispatcher, LayoutId, Model, ModelContext, + Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, + PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, + RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, + Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, + WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -60,16 +59,7 @@ pub enum DispatchPhase { } type AnyObserver = Box bool + 'static>; -type AnyListener = Box; -type AnyKeyListener = Box< - dyn Fn( - &dyn Any, - &[&KeyBindingContext], - DispatchPhase, - &mut WindowContext, - ) -> Option> - + 'static, ->; +type AnyMouseListener = Box; type AnyFocusListener = Box; type AnyWindowFocusListener = Box bool + 'static>; @@ -97,20 +87,12 @@ impl FocusId { /// Obtains whether this handle contains the given handle in the most recently rendered frame. pub(crate) fn contains(&self, other: Self, cx: &WindowContext) -> bool { - let mut ancestor = Some(other); - while let Some(ancestor_id) = ancestor { - if *self == ancestor_id { - return true; - } else { - ancestor = cx - .window - .current_frame - .focus_parents_by_child - .get(&ancestor_id) - .copied(); - } - } - false + cx.window + .current_frame + .key_dispatcher + .as_ref() + .unwrap() + .focus_contains(*self, other) } } @@ -227,20 +209,31 @@ pub struct Window { pub(crate) focus: Option, } -#[derive(Default)] +// #[derive(Default)] pub(crate) struct Frame { element_states: HashMap, - key_matchers: HashMap, - mouse_listeners: HashMap>, + mouse_listeners: HashMap>, + pub(crate) key_dispatcher: Option, pub(crate) focus_listeners: Vec, - pub(crate) key_dispatch_stack: Vec, - freeze_key_dispatch_stack: bool, - focus_parents_by_child: HashMap, pub(crate) scene_builder: SceneBuilder, z_index_stack: StackingOrder, content_mask_stack: Vec>, element_offset_stack: Vec>, - focus_stack: Vec, +} + +impl Frame { + pub fn new(key_dispatcher: KeyDispatcher) -> Self { + Frame { + element_states: HashMap::default(), + mouse_listeners: HashMap::default(), + key_dispatcher: Some(key_dispatcher), + focus_listeners: Vec::new(), + scene_builder: SceneBuilder::default(), + z_index_stack: StackingOrder::default(), + content_mask_stack: Vec::new(), + element_offset_stack: Vec::new(), + } + } } impl Window { @@ -309,8 +302,8 @@ impl Window { layout_engine: TaffyLayoutEngine::new(), root_view: None, element_id_stack: GlobalElementId::default(), - previous_frame: Frame::default(), - current_frame: Frame::default(), + previous_frame: Frame::new(KeyDispatcher::new(cx.keymap.clone())), + current_frame: Frame::new(KeyDispatcher::new(cx.keymap.clone())), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), focus_listeners: SubscriberSet::new(), default_prevented: true, @@ -328,18 +321,6 @@ impl Window { } } -/// When constructing the element tree, we maintain a stack of key dispatch frames until we -/// find the focused element. We interleave key listeners with dispatch contexts so we can use the -/// contexts when matching key events against the keymap. A key listener can be either an action -/// handler or a [KeyDown] / [KeyUp] event listener. -pub(crate) enum KeyDispatchStackFrame { - Listener { - event_type: TypeId, - listener: AnyKeyListener, - }, - Context(KeyBindingContext), -} - /// Indicates which region of the window is visible. Content falling outside of this mask will not be /// rendered. Currently, only rectangular content masks are supported, but we give the mask its own type /// to leave room to support more complex shapes in the future. @@ -407,7 +388,9 @@ impl<'a> WindowContext<'a> { /// Move focus to the element associated with the given `FocusHandle`. pub fn focus(&mut self, handle: &FocusHandle) { - if self.window.focus == Some(handle.id) { + let focus_id = handle.id; + + if self.window.focus == Some(focus_id) { return; } @@ -415,13 +398,10 @@ impl<'a> WindowContext<'a> { self.window.last_blur = Some(self.window.focus); } - self.window.focus = Some(handle.id); - - // self.window.current_frame.key_dispatch_stack.clear() - // self.window.root_view.initialize() + self.window.focus = Some(focus_id); self.app.push_effect(Effect::FocusChanged { window_handle: self.window.handle, - focused: Some(handle.id), + focused: Some(focus_id), }); self.notify(); } @@ -441,11 +421,13 @@ impl<'a> WindowContext<'a> { } pub fn dispatch_action(&mut self, action: Box) { - self.defer(|cx| { - cx.app.propagate_event = true; - let stack = cx.dispatch_stack(); - cx.dispatch_action_internal(action, &stack[..]) - }) + if let Some(focus_handle) = self.focused() { + self.defer(move |cx| { + let dispatcher = cx.window.current_frame.key_dispatcher.take().unwrap(); + dispatcher.dispatch_action(focus_handle.id, action, cx); + cx.window.current_frame.key_dispatcher = Some(dispatcher); + }) + } } /// Schedules the given function to be run at the end of the current effect cycle, allowing entities @@ -1079,26 +1061,6 @@ impl<'a> WindowContext<'a> { self.window.dirty = false; } - pub(crate) fn dispatch_stack(&mut self) -> Vec { - let root_view = self.window.root_view.take().unwrap(); - let window = &mut *self.window; - let mut spare_frame = Frame::default(); - mem::swap(&mut spare_frame, &mut window.previous_frame); - - self.start_frame(); - - root_view.draw_dispatch_stack(self); - - let window = &mut *self.window; - // restore the old values of current and previous frame, - // putting the new frame into spare_frame. - mem::swap(&mut window.current_frame, &mut window.previous_frame); - mem::swap(&mut spare_frame, &mut window.previous_frame); - self.window.root_view = Some(root_view); - - spare_frame.key_dispatch_stack - } - /// Rotate the current frame and the previous frame, then clear the current frame. /// We repopulate all state in the current frame during each paint. fn start_frame(&mut self) { @@ -1110,12 +1072,9 @@ impl<'a> WindowContext<'a> { mem::swap(&mut window.previous_frame, &mut window.current_frame); let frame = &mut window.current_frame; frame.element_states.clear(); - frame.key_matchers.clear(); frame.mouse_listeners.values_mut().for_each(Vec::clear); frame.focus_listeners.clear(); - frame.key_dispatch_stack.clear(); - frame.focus_parents_by_child.clear(); - frame.freeze_key_dispatch_stack = false; + frame.key_dispatcher.as_mut().map(KeyDispatcher::clear); } /// Dispatch a mouse or keyboard event on the window. @@ -1226,99 +1185,16 @@ impl<'a> WindowContext<'a> { .insert(any_mouse_event.type_id(), handlers); } } else if let Some(any_key_event) = event.keyboard_event() { - let key_dispatch_stack = mem::take(&mut self.window.current_frame.key_dispatch_stack); - let key_event_type = any_key_event.type_id(); - let mut context_stack = SmallVec::<[&KeyBindingContext; 16]>::new(); - - for (ix, frame) in key_dispatch_stack.iter().enumerate() { - match frame { - KeyDispatchStackFrame::Listener { - event_type, - listener, - } => { - if key_event_type == *event_type { - if let Some(action) = listener( - any_key_event, - &context_stack, - DispatchPhase::Capture, - self, - ) { - self.dispatch_action_internal(action, &key_dispatch_stack[..ix]); - } - if !self.app.propagate_event { - break; - } - } - } - KeyDispatchStackFrame::Context(context) => { - context_stack.push(&context); - } - } + if let Some(focus_id) = self.window.focus { + let mut dispatcher = self.window.current_frame.key_dispatcher.take().unwrap(); + dispatcher.dispatch_key(focus_id, any_key_event, self); + self.window.current_frame.key_dispatcher = Some(dispatcher); } - - if self.app.propagate_event { - for (ix, frame) in key_dispatch_stack.iter().enumerate().rev() { - match frame { - KeyDispatchStackFrame::Listener { - event_type, - listener, - } => { - if key_event_type == *event_type { - if let Some(action) = listener( - any_key_event, - &context_stack, - DispatchPhase::Bubble, - self, - ) { - self.dispatch_action_internal( - action, - &key_dispatch_stack[..ix], - ); - } - - if !self.app.propagate_event { - break; - } - } - } - KeyDispatchStackFrame::Context(_) => { - context_stack.pop(); - } - } - } - } - - drop(context_stack); - self.window.current_frame.key_dispatch_stack = key_dispatch_stack; } !self.app.propagate_event } - /// Attempt to map a keystroke to an action based on the keymap. - pub fn match_keystroke( - &mut self, - element_id: &GlobalElementId, - keystroke: &Keystroke, - context_stack: &[KeyBindingContext], - ) -> KeyMatch { - let key_match = self - .window - .current_frame - .key_matchers - .get_mut(element_id) - .unwrap() - .match_keystroke(keystroke, context_stack); - - if key_match.is_some() { - for matcher in self.window.current_frame.key_matchers.values_mut() { - matcher.clear_pending(); - } - } - - key_match - } - /// Register the given handler to be invoked whenever the global of the given type /// is updated. pub fn observe_global( @@ -1345,105 +1221,16 @@ impl<'a> WindowContext<'a> { self.window.platform_window.prompt(level, msg, answers) } - pub fn available_actions(&self) -> impl Iterator> + '_ { - let key_dispatch_stack = &self.window.previous_frame.key_dispatch_stack; - key_dispatch_stack.iter().filter_map(|frame| { - match frame { - // todo!factor out a KeyDispatchStackFrame::Action - KeyDispatchStackFrame::Listener { - event_type, - listener: _, - } => { - match build_action_from_type(event_type) { - Ok(action) => Some(action), - Err(err) => { - dbg!(err); - None - } // we'll hit his if TypeId == KeyDown - } - } - KeyDispatchStackFrame::Context(_) => None, - } - }) - } - - pub(crate) fn dispatch_action_internal( - &mut self, - action: Box, - dispatch_stack: &[KeyDispatchStackFrame], - ) { - let action_type = action.as_any().type_id(); - - if let Some(mut global_listeners) = self.app.global_action_listeners.remove(&action_type) { - for listener in &global_listeners { - listener(action.as_ref(), DispatchPhase::Capture, self); - if !self.app.propagate_event { - break; - } - } - global_listeners.extend( - self.global_action_listeners - .remove(&action_type) - .unwrap_or_default(), - ); - self.global_action_listeners - .insert(action_type, global_listeners); - } - - if self.app.propagate_event { - for stack_frame in dispatch_stack { - if let KeyDispatchStackFrame::Listener { - event_type, - listener, - } = stack_frame - { - if action_type == *event_type { - listener(action.as_any(), &[], DispatchPhase::Capture, self); - if !self.app.propagate_event { - break; - } - } - } - } - } - - if self.app.propagate_event { - for stack_frame in dispatch_stack.iter().rev() { - if let KeyDispatchStackFrame::Listener { - event_type, - listener, - } = stack_frame - { - if action_type == *event_type { - self.app.propagate_event = false; - listener(action.as_any(), &[], DispatchPhase::Bubble, self); - if !self.app.propagate_event { - break; - } - } - } - } - } - - if self.app.propagate_event { - if let Some(mut global_listeners) = - self.app.global_action_listeners.remove(&action_type) - { - for listener in global_listeners.iter().rev() { - self.app.propagate_event = false; - listener(action.as_ref(), DispatchPhase::Bubble, self); - if !self.app.propagate_event { - break; - } - } - global_listeners.extend( - self.global_action_listeners - .remove(&action_type) - .unwrap_or_default(), - ); - self.global_action_listeners - .insert(action_type, global_listeners); - } + pub fn available_actions(&self) -> Vec> { + if let Some(focus_id) = self.window.focus { + self.window + .current_frame + .key_dispatcher + .as_ref() + .unwrap() + .available_actions(focus_id) + } else { + Vec::new() } } } @@ -1609,22 +1396,9 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { id: impl Into, f: impl FnOnce(GlobalElementId, &mut Self) -> R, ) -> R { - let keymap = self.app_mut().keymap.clone(); let window = self.window_mut(); window.element_id_stack.push(id.into()); let global_id = window.element_id_stack.clone(); - - if window.current_frame.key_matchers.get(&global_id).is_none() { - window.current_frame.key_matchers.insert( - global_id.clone(), - window - .previous_frame - .key_matchers - .remove(&global_id) - .unwrap_or_else(|| KeystrokeMatcher::new(keymap)), - ); - } - let result = f(global_id, self); let window: &mut Window = self.borrow_mut(); window.element_id_stack.pop(); @@ -2109,94 +1883,25 @@ impl<'a, V: 'static> ViewContext<'a, V> { })); } - pub fn with_key_listeners( + pub fn with_key_dispatch( &mut self, - key_listeners: impl IntoIterator)>, - f: impl FnOnce(&mut Self) -> R, + context: KeyContext, + focus_handle: Option, + f: impl FnOnce(Option, &mut Self) -> R, ) -> R { - let old_stack_len = self.window.current_frame.key_dispatch_stack.len(); - if !self.window.current_frame.freeze_key_dispatch_stack { - for (event_type, listener) in key_listeners { - let handle = self.view().downgrade(); - let listener = Box::new( - move |event: &dyn Any, - context_stack: &[&KeyBindingContext], - phase: DispatchPhase, - cx: &mut WindowContext<'_>| { - handle - .update(cx, |view, cx| { - listener(view, event, context_stack, phase, cx) - }) - .log_err() - .flatten() - }, - ); - self.window.current_frame.key_dispatch_stack.push( - KeyDispatchStackFrame::Listener { - event_type, - listener, - }, - ); - } + let mut old_dispatcher = self.window.previous_frame.key_dispatcher.take().unwrap(); + let mut current_dispatcher = self.window.current_frame.key_dispatcher.take().unwrap(); + + current_dispatcher.push_node(context, &mut old_dispatcher); + if let Some(focus_handle) = focus_handle.as_ref() { + current_dispatcher.make_focusable(focus_handle.id); } + let result = f(focus_handle, self); + current_dispatcher.pop_node(); - let result = f(self); + self.window.previous_frame.key_dispatcher = Some(old_dispatcher); + self.window.current_frame.key_dispatcher = Some(current_dispatcher); - if !self.window.current_frame.freeze_key_dispatch_stack { - self.window - .current_frame - .key_dispatch_stack - .truncate(old_stack_len); - } - - result - } - - pub fn with_key_dispatch_context( - &mut self, - context: KeyBindingContext, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - if context.is_empty() { - return f(self); - } - - if !self.window.current_frame.freeze_key_dispatch_stack { - self.window - .current_frame - .key_dispatch_stack - .push(KeyDispatchStackFrame::Context(context)); - } - - let result = f(self); - - if !self.window.previous_frame.freeze_key_dispatch_stack { - self.window.previous_frame.key_dispatch_stack.pop(); - } - - result - } - - pub fn with_focus( - &mut self, - focus_handle: FocusHandle, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - if let Some(parent_focus_id) = self.window.current_frame.focus_stack.last().copied() { - self.window - .current_frame - .focus_parents_by_child - .insert(focus_handle.id, parent_focus_id); - } - self.window.current_frame.focus_stack.push(focus_handle.id); - - if Some(focus_handle.id) == self.window.focus { - self.window.current_frame.freeze_key_dispatch_stack = true; - } - - let result = f(self); - - self.window.current_frame.focus_stack.pop(); result } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 9d0019b2dc..62c5308dec 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,8 +1,8 @@ use editor::Editor; use gpui::{ - div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity, - StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, - WindowContext, + div, uniform_list, Component, Div, FocusableKeyDispatch, ParentElement, Render, + StatefulInteractivity, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, + ViewContext, VisualContext, WindowContext, }; use std::cmp; use theme::ActiveTheme; @@ -137,7 +137,7 @@ impl Picker { } impl Render for Picker { - type Element = Div, FocusEnabled>; + type Element = Div, FocusableKeyDispatch>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 984ee421db..368fb20fbf 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -1,5 +1,5 @@ use gpui::{ - actions, div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render, + actions, div, Div, Focusable, FocusableKeyDispatch, KeyBinding, ParentElement, Render, StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, WindowContext, }; use theme2::ActiveTheme; @@ -21,7 +21,7 @@ impl FocusStory { } impl Render for FocusStory { - type Element = Div, FocusEnabled>; + type Element = Div, FocusableKeyDispatch>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index 543781ef52..980b31fe5d 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -1,4 +1,4 @@ -use gpui::{Div, ElementFocus, ElementInteractivity, Styled}; +use gpui::{Div, ElementInteractivity, KeyDispatch, Styled}; use crate::UITextSize; @@ -69,6 +69,6 @@ pub trait StyledExt: Styled { impl StyledExt for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 1522b4ec4e..e55d59303d 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -38,10 +38,10 @@ use futures::{ use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, GlobalPixels, KeyBindingContext, Model, ModelContext, ParentElement, Point, - Render, Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, - Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, - WindowHandle, WindowOptions, + FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, + StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription, Task, + View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, + WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3743,158 +3743,158 @@ impl Render for Workspace { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let mut context = KeyBindingContext::default(); + let mut context = KeyContext::default(); context.add("Workspace"); - cx.with_key_dispatch_context(context, |cx| { - div() - .relative() - .size_full() - .flex() - .flex_col() - .font("Zed Sans") - .gap_0() - .justify_start() - .items_start() - .text_color(cx.theme().colors().text) - .bg(cx.theme().colors().background) - .child(self.render_titlebar(cx)) - .child( - // todo! should this be a component a view? - self.add_workspace_actions_listeners(div().id("workspace")) - .relative() - .flex_1() - .w_full() - .flex() - .overflow_hidden() - .border_t() - .border_b() - .border_color(cx.theme().colors().border) - .child(self.modal_layer.clone()) - // .children( - // Some( - // Panel::new("project-panel-outer", cx) - // .side(PanelSide::Left) - // .child(ProjectPanel::new("project-panel-inner")), - // ) - // .filter(|_| self.is_project_panel_open()), - // ) - // .children( - // Some( - // Panel::new("collab-panel-outer", cx) - // .child(CollabPanel::new("collab-panel-inner")) - // .side(PanelSide::Left), - // ) - // .filter(|_| self.is_collab_panel_open()), - // ) - // .child(NotificationToast::new( - // "maxbrunsfeld has requested to add you as a contact.".into(), - // )) - .child( - div().flex().flex_col().flex_1().h_full().child( - div().flex().flex_1().child(self.center.render( - &self.project, - &self.follower_states, - self.active_call(), - &self.active_pane, - self.zoomed.as_ref(), - &self.app_state, - cx, - )), - ), // .children( - // Some( - // Panel::new("terminal-panel", cx) - // .child(Terminal::new()) - // .allowed_sides(PanelAllowedSides::BottomOnly) - // .side(PanelSide::Bottom), - // ) - // .filter(|_| self.is_terminal_open()), - // ), + + div() + .context(context) + .relative() + .size_full() + .flex() + .flex_col() + .font("Zed Sans") + .gap_0() + .justify_start() + .items_start() + .text_color(cx.theme().colors().text) + .bg(cx.theme().colors().background) + .child(self.render_titlebar(cx)) + .child( + // todo! should this be a component a view? + self.add_workspace_actions_listeners(div().id("workspace")) + .relative() + .flex_1() + .w_full() + .flex() + .overflow_hidden() + .border_t() + .border_b() + .border_color(cx.theme().colors().border) + .child(self.modal_layer.clone()) + // .children( + // Some( + // Panel::new("project-panel-outer", cx) + // .side(PanelSide::Left) + // .child(ProjectPanel::new("project-panel-inner")), + // ) + // .filter(|_| self.is_project_panel_open()), + // ) + // .children( + // Some( + // Panel::new("collab-panel-outer", cx) + // .child(CollabPanel::new("collab-panel-inner")) + // .side(PanelSide::Left), + // ) + // .filter(|_| self.is_collab_panel_open()), + // ) + // .child(NotificationToast::new( + // "maxbrunsfeld has requested to add you as a contact.".into(), + // )) + .child( + div().flex().flex_col().flex_1().h_full().child( + div().flex().flex_1().child(self.center.render( + &self.project, + &self.follower_states, + self.active_call(), + &self.active_pane, + self.zoomed.as_ref(), + &self.app_state, + cx, + )), ), // .children( // Some( - // Panel::new("chat-panel-outer", cx) - // .side(PanelSide::Right) - // .child(ChatPanel::new("chat-panel-inner").messages(vec![ - // ChatMessage::new( - // "osiewicz".to_string(), - // "is this thing on?".to_string(), - // DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") - // .unwrap() - // .naive_local(), - // ), - // ChatMessage::new( - // "maxdeviant".to_string(), - // "Reading you loud and clear!".to_string(), - // DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") - // .unwrap() - // .naive_local(), - // ), - // ])), + // Panel::new("terminal-panel", cx) + // .child(Terminal::new()) + // .allowed_sides(PanelAllowedSides::BottomOnly) + // .side(PanelSide::Bottom), // ) - // .filter(|_| self.is_chat_panel_open()), - // ) - // .children( - // Some( - // Panel::new("notifications-panel-outer", cx) - // .side(PanelSide::Right) - // .child(NotificationsPanel::new("notifications-panel-inner")), - // ) - // .filter(|_| self.is_notifications_panel_open()), - // ) - // .children( - // Some( - // Panel::new("assistant-panel-outer", cx) - // .child(AssistantPanel::new("assistant-panel-inner")), - // ) - // .filter(|_| self.is_assistant_panel_open()), + // .filter(|_| self.is_terminal_open()), // ), - ) - .child(self.status_bar.clone()) - // .when(self.debug.show_toast, |this| { - // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) - // }) - // .children( - // Some( - // div() - // .absolute() - // .top(px(50.)) - // .left(px(640.)) - // .z_index(8) - // .child(LanguageSelector::new("language-selector")), - // ) - // .filter(|_| self.is_language_selector_open()), - // ) - .z_index(8) - // Debug - .child( - div() - .flex() - .flex_col() - .z_index(9) - .absolute() - .top_20() - .left_1_4() - .w_40() - .gap_2(), // .when(self.show_debug, |this| { - // this.child(Button::::new("Toggle User Settings").on_click( - // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), - // )) - // .child( - // Button::::new("Toggle Toasts").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_toast(cx), - // )), - // ) - // .child( - // Button::::new("Toggle Livestream").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_livestream(cx), - // )), - // ) - // }) - // .child( - // Button::::new("Toggle Debug") - // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), - // ), - ) - }) + ), // .children( + // Some( + // Panel::new("chat-panel-outer", cx) + // .side(PanelSide::Right) + // .child(ChatPanel::new("chat-panel-inner").messages(vec![ + // ChatMessage::new( + // "osiewicz".to_string(), + // "is this thing on?".to_string(), + // DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") + // .unwrap() + // .naive_local(), + // ), + // ChatMessage::new( + // "maxdeviant".to_string(), + // "Reading you loud and clear!".to_string(), + // DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") + // .unwrap() + // .naive_local(), + // ), + // ])), + // ) + // .filter(|_| self.is_chat_panel_open()), + // ) + // .children( + // Some( + // Panel::new("notifications-panel-outer", cx) + // .side(PanelSide::Right) + // .child(NotificationsPanel::new("notifications-panel-inner")), + // ) + // .filter(|_| self.is_notifications_panel_open()), + // ) + // .children( + // Some( + // Panel::new("assistant-panel-outer", cx) + // .child(AssistantPanel::new("assistant-panel-inner")), + // ) + // .filter(|_| self.is_assistant_panel_open()), + // ), + ) + .child(self.status_bar.clone()) + // .when(self.debug.show_toast, |this| { + // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) + // }) + // .children( + // Some( + // div() + // .absolute() + // .top(px(50.)) + // .left(px(640.)) + // .z_index(8) + // .child(LanguageSelector::new("language-selector")), + // ) + // .filter(|_| self.is_language_selector_open()), + // ) + .z_index(8) + // Debug + .child( + div() + .flex() + .flex_col() + .z_index(9) + .absolute() + .top_20() + .left_1_4() + .w_40() + .gap_2(), // .when(self.show_debug, |this| { + // this.child(Button::::new("Toggle User Settings").on_click( + // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), + // )) + // .child( + // Button::::new("Toggle Toasts").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_toast(cx), + // )), + // ) + // .child( + // Button::::new("Toggle Livestream").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_livestream(cx), + // )), + // ) + // }) + // .child( + // Button::::new("Toggle Debug") + // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), + // ), + ) } } // todo!() From 13255ef133c5383f81bc2db2dc2ea0efd92c9dd2 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 12 Nov 2023 23:22:02 -0800 Subject: [PATCH 012/126] Poke at getting the project panel showing up --- crates/project_panel2/src/project_panel.rs | 66 ++++--- crates/settings2/src/keymap_file.rs | 6 +- crates/workspace2/src/dock.rs | 14 +- crates/workspace2/src/modal_layer.rs | 24 --- crates/workspace2/src/workspace2.rs | 204 +++++++++++---------- 5 files changed, 157 insertions(+), 157 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 0844f5190c..e3e04f5254 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -9,10 +9,10 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ actions, div, px, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, - AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Model, - ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, - StatefulInteractivity, Styled, Task, UniformListScrollHandle, View, ViewContext, - VisualContext as _, WeakView, WindowContext, + AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusEnabled, + FocusHandle, Model, ParentElement as _, Pixels, Point, PromptLevel, Render, + StatefulInteractive, StatefulInteractivity, Styled, Task, UniformListScrollHandle, View, + ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -131,6 +131,7 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); file_associations::init(assets, cx); + // cx.add_action(ProjectPanel::expand_selected_entry); // cx.add_action(ProjectPanel::collapse_selected_entry); // cx.add_action(ProjectPanel::collapse_all_entries); @@ -1437,7 +1438,7 @@ impl ProjectPanel { } impl Render for ProjectPanel { - type Element = Div>; + type Element = Div, FocusEnabled>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { enum ProjectPanel {} @@ -1447,31 +1448,36 @@ impl Render for ProjectPanel { let has_worktree = self.visible_entries.len() != 0; if has_worktree { - div().id("project-panel").child( - uniform_list( - "entries", - self.visible_entries - .iter() - .map(|(_, worktree_entries)| worktree_entries.len()) - .sum(), - |this: &mut Self, range, cx| { - let mut items = SmallVec::new(); - this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(Self::render_entry( - id, - details, - &this.filename_editor, - // &mut dragged_entry_destination, - cx, - )); - }); - items - }, + div() + .id("project-panel") + .track_focus(&self.focus_handle) + .child( + uniform_list( + "entries", + self.visible_entries + .iter() + .map(|(_, worktree_entries)| worktree_entries.len()) + .sum(), + |this: &mut Self, range, cx| { + let mut items = SmallVec::new(); + this.for_each_visible_entry(range, cx, |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + // &mut dragged_entry_destination, + cx, + )); + }); + items + }, + ) + .track_scroll(self.list.clone()), ) - .track_scroll(self.list.clone()), - ) } else { - v_stack().id("empty-project_panel") + v_stack() + .id("empty-project_panel") + .track_focus(&self.focus_handle) } } } @@ -1537,6 +1543,10 @@ impl workspace::dock::Panel for ProjectPanel { "Project Panel" } + fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle { + self.focus_handle.clone() + } + // fn is_focus_event(event: &Self::Event) -> bool { // matches!(event, Event::Focus) // } diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs index 9f279864ee..2b57af0fdb 100644 --- a/crates/settings2/src/keymap_file.rs +++ b/crates/settings2/src/keymap_file.rs @@ -9,7 +9,7 @@ use schemars::{ }; use serde::Deserialize; use serde_json::Value; -use util::{asset_str, ResultExt}; +use util::asset_str; #[derive(Debug, Deserialize, Default, Clone, JsonSchema)] #[serde(transparent)] @@ -86,7 +86,9 @@ impl KeymapFile { "invalid binding value for keystroke {keystroke}, context {context:?}" ) }) - .log_err() + // todo!() + .ok() + // .log_err() .map(|action| KeyBinding::load(&keystroke, action, context.as_deref())) }) .collect::>>()?; diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index f21eb84ae2..c8ff58f42d 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,7 +1,7 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, ParentElement, Render, - Subscription, View, ViewContext, WeakView, WindowContext, + div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, FocusHandle, + ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -34,6 +34,7 @@ pub trait Panel: Render + EventEmitter { fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} fn has_focus(&self, cx: &WindowContext) -> bool; + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; } pub trait PanelHandle: Send + Sync { @@ -51,6 +52,7 @@ pub trait PanelHandle: Send + Sync { fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>); fn icon_label(&self, cx: &WindowContext) -> Option; fn has_focus(&self, cx: &WindowContext) -> bool; + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn to_any(&self) -> AnyView; } @@ -117,6 +119,10 @@ where fn to_any(&self) -> AnyView { self.clone().into() } + + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + self.read(cx).focus_handle(cx).clone() + } } impl From<&dyn PanelHandle> for AnyView { @@ -728,5 +734,9 @@ pub mod test { fn has_focus(&self, _cx: &WindowContext) -> bool { self.has_focus } + + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + unimplemented!() + } } } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index a5760380f5..f197718b59 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -89,27 +89,3 @@ impl ModalLayer { }) } } - -// impl Render for ModalLayer { -// type Element = Div; - -// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { -// let mut div = div(); -// for (type_id, build_view) in cx.global::().registered_modals { -// div = div.useful_on_action( -// type_id, -// Box::new(|this, _: dyn Any, phase, cx: &mut ViewContext| { -// if phase == DispatchPhase::Capture { -// return; -// } -// self.workspace.update(cx, |workspace, cx| { -// self.open_modal = Some(build_view(workspace, cx)); -// }); -// cx.notify(); -// }), -// ) -// } - -// div -// } -// } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index df2da095ee..c9fd322ed3 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -39,8 +39,8 @@ use gpui::{ actions, div, point, rems, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, - Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, - WindowContext, WindowHandle, WindowOptions, + StatelessInteractive, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -247,102 +247,6 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { // } // } // }); - // cx.add_async_action(Workspace::open); - - // cx.add_async_action(Workspace::follow_next_collaborator); - // cx.add_async_action(Workspace::close); - // cx.add_async_action(Workspace::close_inactive_items_and_panes); - // cx.add_async_action(Workspace::close_all_items_and_panes); - // cx.add_global_action(Workspace::close_global); - // cx.add_global_action(restart); - // cx.add_async_action(Workspace::save_all); - // cx.add_action(Workspace::add_folder_to_project); - // cx.add_action( - // |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { - // let pane = workspace.active_pane().clone(); - // workspace.unfollow(&pane, cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { - // workspace - // .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { - // workspace - // .save_active_item(SaveIntent::SaveAs, cx) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { - // workspace.activate_previous_pane(cx) - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { - // workspace.activate_next_pane(cx) - // }); - - // cx.add_action( - // |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { - // workspace.activate_pane_in_direction(action.0, cx) - // }, - // ); - - // cx.add_action( - // |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { - // workspace.swap_pane_in_direction(action.0, cx) - // }, - // ); - - // cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| { - // workspace.toggle_dock(DockPosition::Left, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { - // workspace.toggle_dock(DockPosition::Right, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { - // workspace.toggle_dock(DockPosition::Bottom, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { - // workspace.close_all_docks(cx); - // }); - // cx.add_action(Workspace::activate_pane_at_index); - // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { - // workspace.reopen_closed_item(cx).detach(); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { - // workspace - // .go_back(workspace.active_pane().downgrade(), cx) - // .detach(); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { - // workspace - // .go_forward(workspace.active_pane().downgrade(), cx) - // .detach(); - // }); - - // cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| { - // cx.spawn(|workspace, mut cx| async move { - // let err = install_cli::install_cli(&cx) - // .await - // .context("Failed to create CLI symlink"); - - // workspace.update(&mut cx, |workspace, cx| { - // if matches!(err, Err(_)) { - // err.notify_err(workspace, cx); - // } else { - // workspace.show_notification(1, cx, |cx| { - // cx.build_view(|_| { - // MessageNotification::new("Successfully installed the `zed` binary") - // }) - // }); - // } - // }) - // }) - // .detach(); - // }); } type ProjectItemBuilders = @@ -1653,7 +1557,8 @@ impl Workspace { focus_center = true; } } else { - // cx.focus(active_panel.as_any()); + let focus_handle = &active_panel.focus_handle(cx); + cx.focus(focus_handle); reveal_dock = true; } } @@ -3350,6 +3255,103 @@ impl Workspace { }) } + fn actions(div: Div) -> Div { + div + // cx.add_async_action(Workspace::open); + // cx.add_async_action(Workspace::follow_next_collaborator); + // cx.add_async_action(Workspace::close); + // cx.add_async_action(Workspace::close_inactive_items_and_panes); + // cx.add_async_action(Workspace::close_all_items_and_panes); + // cx.add_global_action(Workspace::close_global); + // cx.add_global_action(restart); + // cx.add_async_action(Workspace::save_all); + // cx.add_action(Workspace::add_folder_to_project); + // cx.add_action( + // |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { + // let pane = workspace.active_pane().clone(); + // workspace.unfollow(&pane, cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { + // workspace + // .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { + // workspace + // .save_active_item(SaveIntent::SaveAs, cx) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { + // workspace.activate_previous_pane(cx) + // }); + // cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { + // workspace.activate_next_pane(cx) + // }); + // cx.add_action( + // |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { + // workspace.activate_pane_in_direction(action.0, cx) + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { + // workspace.swap_pane_in_direction(action.0, cx) + // }, + // ); + .on_action(|this, e: &ToggleLeftDock, cx| { + println!("TOGGLING DOCK"); + this.toggle_dock(DockPosition::Left, cx); + }) + // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { + // workspace.toggle_dock(DockPosition::Right, cx); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { + // workspace.toggle_dock(DockPosition::Bottom, cx); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { + // workspace.close_all_docks(cx); + // }); + // cx.add_action(Workspace::activate_pane_at_index); + // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { + // workspace.reopen_closed_item(cx).detach(); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { + // workspace + // .go_back(workspace.active_pane().downgrade(), cx) + // .detach(); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { + // workspace + // .go_forward(workspace.active_pane().downgrade(), cx) + // .detach(); + // }); + + // cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| { + // cx.spawn(|workspace, mut cx| async move { + // let err = install_cli::install_cli(&cx) + // .await + // .context("Failed to create CLI symlink"); + + // workspace.update(&mut cx, |workspace, cx| { + // if matches!(err, Err(_)) { + // err.notify_err(workspace, cx); + // } else { + // workspace.show_notification(1, cx, |cx| { + // cx.build_view(|_| { + // MessageNotification::new("Successfully installed the `zed` binary") + // }) + // }); + // } + // }) + // }) + // .detach(); + // }); + } + // todo!() // #[cfg(any(test, feature = "test-support"))] // pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { @@ -3628,7 +3630,7 @@ impl Render for Workspace { .text_color(cx.theme().colors().text) .bg(cx.theme().colors().background) .child(self.render_titlebar(cx)) - .child( + .child(Workspace::actions( // todo! should this be a component a view? self.modal_layer .wrapper_element(cx) @@ -3717,7 +3719,7 @@ impl Render for Workspace { // ) // .filter(|_| self.is_assistant_panel_open()), // ), - ) + )) .child(self.status_bar.clone()) // .when(self.debug.show_toast, |this| { // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) From 318cb784b2e7fbaca99504d879a208a18590c97c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 10:17:52 +0100 Subject: [PATCH 013/126] Fix panic when calling `with_key_dispatch` recursively Co-Authored-By: Thorsten --- crates/gpui2/src/window.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 15a59253e7..3da2664f79 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1889,18 +1889,18 @@ impl<'a, V: 'static> ViewContext<'a, V> { focus_handle: Option, f: impl FnOnce(Option, &mut Self) -> R, ) -> R { - let mut old_dispatcher = self.window.previous_frame.key_dispatcher.take().unwrap(); - let mut current_dispatcher = self.window.current_frame.key_dispatcher.take().unwrap(); + let window = &mut self.window; + let old_dispatcher = window.previous_frame.key_dispatcher.as_mut().unwrap(); + let current_dispatcher = window.current_frame.key_dispatcher.as_mut().unwrap(); - current_dispatcher.push_node(context, &mut old_dispatcher); + current_dispatcher.push_node(context, old_dispatcher); if let Some(focus_handle) = focus_handle.as_ref() { current_dispatcher.make_focusable(focus_handle.id); } let result = f(focus_handle, self); - current_dispatcher.pop_node(); - self.window.previous_frame.key_dispatcher = Some(old_dispatcher); - self.window.current_frame.key_dispatcher = Some(current_dispatcher); + let current_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); + current_dispatcher.pop_node(); result } From 9c182538638ad82690342ef4f8598e8349a3d1bb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 11:37:57 +0100 Subject: [PATCH 014/126] Register key and action listeners using `Interactive::initialize` Co-Authored-By: Thorsten --- crates/editor2/src/element.rs | 362 ++++++++++++------------- crates/gpui2/src/elements/div.rs | 1 + crates/gpui2/src/interactive.rs | 87 +++--- crates/gpui2/src/key_dispatch.rs | 50 ++-- crates/gpui2/src/window.rs | 62 +++++ crates/storybook2/src/stories/focus.rs | 6 +- 6 files changed, 317 insertions(+), 251 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index d42a14bb77..b273c5914a 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -15,10 +15,10 @@ use crate::{ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ - black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, - BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, - ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, - KeyContext, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton, + black, hsla, point, px, relative, size, transparent_black, Action, ActionListener, AnyElement, + AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, + Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, + InputHandler, KeyContext, KeyDownEvent, KeyMatch, Line, LineLayout, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size, Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout, }; @@ -2459,7 +2459,166 @@ impl Element for EditorElement { cx.with_key_dispatch( dispatch_context, Some(editor.focus_handle.clone()), - |_, _| {}, + |_, cx| { + handle_action(cx, Editor::move_left); + handle_action(cx, Editor::move_right); + handle_action(cx, Editor::move_down); + handle_action(cx, Editor::move_up); + // on_action(cx, Editor::new_file); todo!() + // on_action(cx, Editor::new_file_in_direction); todo!() + handle_action(cx, Editor::cancel); + handle_action(cx, Editor::newline); + handle_action(cx, Editor::newline_above); + handle_action(cx, Editor::newline_below); + handle_action(cx, Editor::backspace); + handle_action(cx, Editor::delete); + handle_action(cx, Editor::tab); + handle_action(cx, Editor::tab_prev); + handle_action(cx, Editor::indent); + handle_action(cx, Editor::outdent); + handle_action(cx, Editor::delete_line); + handle_action(cx, Editor::join_lines); + handle_action(cx, Editor::sort_lines_case_sensitive); + handle_action(cx, Editor::sort_lines_case_insensitive); + handle_action(cx, Editor::reverse_lines); + handle_action(cx, Editor::shuffle_lines); + handle_action(cx, Editor::convert_to_upper_case); + handle_action(cx, Editor::convert_to_lower_case); + handle_action(cx, Editor::convert_to_title_case); + handle_action(cx, Editor::convert_to_snake_case); + handle_action(cx, Editor::convert_to_kebab_case); + handle_action(cx, Editor::convert_to_upper_camel_case); + handle_action(cx, Editor::convert_to_lower_camel_case); + handle_action(cx, Editor::delete_to_previous_word_start); + handle_action(cx, Editor::delete_to_previous_subword_start); + handle_action(cx, Editor::delete_to_next_word_end); + handle_action(cx, Editor::delete_to_next_subword_end); + handle_action(cx, Editor::delete_to_beginning_of_line); + handle_action(cx, Editor::delete_to_end_of_line); + handle_action(cx, Editor::cut_to_end_of_line); + handle_action(cx, Editor::duplicate_line); + handle_action(cx, Editor::move_line_up); + handle_action(cx, Editor::move_line_down); + handle_action(cx, Editor::transpose); + handle_action(cx, Editor::cut); + handle_action(cx, Editor::copy); + handle_action(cx, Editor::paste); + handle_action(cx, Editor::undo); + handle_action(cx, Editor::redo); + handle_action(cx, Editor::move_page_up); + handle_action(cx, Editor::move_page_down); + handle_action(cx, Editor::next_screen); + handle_action(cx, Editor::scroll_cursor_top); + handle_action(cx, Editor::scroll_cursor_center); + handle_action(cx, Editor::scroll_cursor_bottom); + handle_action(cx, |editor, _: &LineDown, cx| { + editor.scroll_screen(&ScrollAmount::Line(1.), cx) + }); + handle_action(cx, |editor, _: &LineUp, cx| { + editor.scroll_screen(&ScrollAmount::Line(-1.), cx) + }); + handle_action(cx, |editor, _: &HalfPageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(0.5), cx) + }); + handle_action(cx, |editor, _: &HalfPageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) + }); + handle_action(cx, |editor, _: &PageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.), cx) + }); + handle_action(cx, |editor, _: &PageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-1.), cx) + }); + handle_action(cx, Editor::move_to_previous_word_start); + handle_action(cx, Editor::move_to_previous_subword_start); + handle_action(cx, Editor::move_to_next_word_end); + handle_action(cx, Editor::move_to_next_subword_end); + handle_action(cx, Editor::move_to_beginning_of_line); + handle_action(cx, Editor::move_to_end_of_line); + handle_action(cx, Editor::move_to_start_of_paragraph); + handle_action(cx, Editor::move_to_end_of_paragraph); + handle_action(cx, Editor::move_to_beginning); + handle_action(cx, Editor::move_to_end); + handle_action(cx, Editor::select_up); + handle_action(cx, Editor::select_down); + handle_action(cx, Editor::select_left); + handle_action(cx, Editor::select_right); + handle_action(cx, Editor::select_to_previous_word_start); + handle_action(cx, Editor::select_to_previous_subword_start); + handle_action(cx, Editor::select_to_next_word_end); + handle_action(cx, Editor::select_to_next_subword_end); + handle_action(cx, Editor::select_to_beginning_of_line); + handle_action(cx, Editor::select_to_end_of_line); + handle_action(cx, Editor::select_to_start_of_paragraph); + handle_action(cx, Editor::select_to_end_of_paragraph); + handle_action(cx, Editor::select_to_beginning); + handle_action(cx, Editor::select_to_end); + handle_action(cx, Editor::select_all); + handle_action(cx, |editor, action, cx| { + editor.select_all_matches(action, cx).log_err(); + }); + handle_action(cx, Editor::select_line); + handle_action(cx, Editor::split_selection_into_lines); + handle_action(cx, Editor::add_selection_above); + handle_action(cx, Editor::add_selection_below); + handle_action(cx, |editor, action, cx| { + editor.select_next(action, cx).log_err(); + }); + handle_action(cx, |editor, action, cx| { + editor.select_previous(action, cx).log_err(); + }); + handle_action(cx, Editor::toggle_comments); + handle_action(cx, Editor::select_larger_syntax_node); + handle_action(cx, Editor::select_smaller_syntax_node); + handle_action(cx, Editor::move_to_enclosing_bracket); + handle_action(cx, Editor::undo_selection); + handle_action(cx, Editor::redo_selection); + handle_action(cx, Editor::go_to_diagnostic); + handle_action(cx, Editor::go_to_prev_diagnostic); + handle_action(cx, Editor::go_to_hunk); + handle_action(cx, Editor::go_to_prev_hunk); + handle_action(cx, Editor::go_to_definition); + handle_action(cx, Editor::go_to_definition_split); + handle_action(cx, Editor::go_to_type_definition); + handle_action(cx, Editor::go_to_type_definition_split); + handle_action(cx, Editor::fold); + handle_action(cx, Editor::fold_at); + handle_action(cx, Editor::unfold_lines); + handle_action(cx, Editor::unfold_at); + handle_action(cx, Editor::fold_selected_ranges); + handle_action(cx, Editor::show_completions); + handle_action(cx, Editor::toggle_code_actions); + // on_action(cx, Editor::open_excerpts); todo!() + handle_action(cx, Editor::toggle_soft_wrap); + handle_action(cx, Editor::toggle_inlay_hints); + handle_action(cx, Editor::reveal_in_finder); + handle_action(cx, Editor::copy_path); + handle_action(cx, Editor::copy_relative_path); + handle_action(cx, Editor::copy_highlight_json); + handle_action(cx, |editor, action, cx| { + editor + .format(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + handle_action(cx, Editor::restart_language_server); + handle_action(cx, Editor::show_character_palette); + // on_action(cx, Editor::confirm_completion); todo!() + handle_action(cx, |editor, action, cx| { + editor + .confirm_code_action(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + // on_action(cx, Editor::rename); todo!() + // on_action(cx, Editor::confirm_rename); todo!() + // on_action(cx, Editor::find_all_references); todo!() + handle_action(cx, Editor::next_copilot_suggestion); + handle_action(cx, Editor::previous_copilot_suggestion); + handle_action(cx, Editor::copilot_suggest); + handle_action(cx, Editor::context_menu_first); + handle_action(cx, Editor::context_menu_prev); + handle_action(cx, Editor::context_menu_next); + handle_action(cx, Editor::context_menu_last); + }, ) }); } @@ -3995,197 +4154,14 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { // } // } -fn build_key_listeners( - global_element_id: GlobalElementId, -) -> impl IntoIterator)> { - [ - build_action_listener(Editor::move_left), - build_action_listener(Editor::move_right), - build_action_listener(Editor::move_down), - build_action_listener(Editor::move_up), - // build_action_listener(Editor::new_file), todo!() - // build_action_listener(Editor::new_file_in_direction), todo!() - build_action_listener(Editor::cancel), - build_action_listener(Editor::newline), - build_action_listener(Editor::newline_above), - build_action_listener(Editor::newline_below), - build_action_listener(Editor::backspace), - build_action_listener(Editor::delete), - build_action_listener(Editor::tab), - build_action_listener(Editor::tab_prev), - build_action_listener(Editor::indent), - build_action_listener(Editor::outdent), - build_action_listener(Editor::delete_line), - build_action_listener(Editor::join_lines), - build_action_listener(Editor::sort_lines_case_sensitive), - build_action_listener(Editor::sort_lines_case_insensitive), - build_action_listener(Editor::reverse_lines), - build_action_listener(Editor::shuffle_lines), - build_action_listener(Editor::convert_to_upper_case), - build_action_listener(Editor::convert_to_lower_case), - build_action_listener(Editor::convert_to_title_case), - build_action_listener(Editor::convert_to_snake_case), - build_action_listener(Editor::convert_to_kebab_case), - build_action_listener(Editor::convert_to_upper_camel_case), - build_action_listener(Editor::convert_to_lower_camel_case), - build_action_listener(Editor::delete_to_previous_word_start), - build_action_listener(Editor::delete_to_previous_subword_start), - build_action_listener(Editor::delete_to_next_word_end), - build_action_listener(Editor::delete_to_next_subword_end), - build_action_listener(Editor::delete_to_beginning_of_line), - build_action_listener(Editor::delete_to_end_of_line), - build_action_listener(Editor::cut_to_end_of_line), - build_action_listener(Editor::duplicate_line), - build_action_listener(Editor::move_line_up), - build_action_listener(Editor::move_line_down), - build_action_listener(Editor::transpose), - build_action_listener(Editor::cut), - build_action_listener(Editor::copy), - build_action_listener(Editor::paste), - build_action_listener(Editor::undo), - build_action_listener(Editor::redo), - build_action_listener(Editor::move_page_up), - build_action_listener(Editor::move_page_down), - build_action_listener(Editor::next_screen), - build_action_listener(Editor::scroll_cursor_top), - build_action_listener(Editor::scroll_cursor_center), - build_action_listener(Editor::scroll_cursor_bottom), - build_action_listener(|editor, _: &LineDown, cx| { - editor.scroll_screen(&ScrollAmount::Line(1.), cx) - }), - build_action_listener(|editor, _: &LineUp, cx| { - editor.scroll_screen(&ScrollAmount::Line(-1.), cx) - }), - build_action_listener(|editor, _: &HalfPageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(0.5), cx) - }), - build_action_listener(|editor, _: &HalfPageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) - }), - build_action_listener(|editor, _: &PageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(1.), cx) - }), - build_action_listener(|editor, _: &PageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-1.), cx) - }), - build_action_listener(Editor::move_to_previous_word_start), - build_action_listener(Editor::move_to_previous_subword_start), - build_action_listener(Editor::move_to_next_word_end), - build_action_listener(Editor::move_to_next_subword_end), - build_action_listener(Editor::move_to_beginning_of_line), - build_action_listener(Editor::move_to_end_of_line), - build_action_listener(Editor::move_to_start_of_paragraph), - build_action_listener(Editor::move_to_end_of_paragraph), - build_action_listener(Editor::move_to_beginning), - build_action_listener(Editor::move_to_end), - build_action_listener(Editor::select_up), - build_action_listener(Editor::select_down), - build_action_listener(Editor::select_left), - build_action_listener(Editor::select_right), - build_action_listener(Editor::select_to_previous_word_start), - build_action_listener(Editor::select_to_previous_subword_start), - build_action_listener(Editor::select_to_next_word_end), - build_action_listener(Editor::select_to_next_subword_end), - build_action_listener(Editor::select_to_beginning_of_line), - build_action_listener(Editor::select_to_end_of_line), - build_action_listener(Editor::select_to_start_of_paragraph), - build_action_listener(Editor::select_to_end_of_paragraph), - build_action_listener(Editor::select_to_beginning), - build_action_listener(Editor::select_to_end), - build_action_listener(Editor::select_all), - build_action_listener(|editor, action, cx| { - editor.select_all_matches(action, cx).log_err(); - }), - build_action_listener(Editor::select_line), - build_action_listener(Editor::split_selection_into_lines), - build_action_listener(Editor::add_selection_above), - build_action_listener(Editor::add_selection_below), - build_action_listener(|editor, action, cx| { - editor.select_next(action, cx).log_err(); - }), - build_action_listener(|editor, action, cx| { - editor.select_previous(action, cx).log_err(); - }), - build_action_listener(Editor::toggle_comments), - build_action_listener(Editor::select_larger_syntax_node), - build_action_listener(Editor::select_smaller_syntax_node), - build_action_listener(Editor::move_to_enclosing_bracket), - build_action_listener(Editor::undo_selection), - build_action_listener(Editor::redo_selection), - build_action_listener(Editor::go_to_diagnostic), - build_action_listener(Editor::go_to_prev_diagnostic), - build_action_listener(Editor::go_to_hunk), - build_action_listener(Editor::go_to_prev_hunk), - build_action_listener(Editor::go_to_definition), - build_action_listener(Editor::go_to_definition_split), - build_action_listener(Editor::go_to_type_definition), - build_action_listener(Editor::go_to_type_definition_split), - build_action_listener(Editor::fold), - build_action_listener(Editor::fold_at), - build_action_listener(Editor::unfold_lines), - build_action_listener(Editor::unfold_at), - build_action_listener(Editor::fold_selected_ranges), - build_action_listener(Editor::show_completions), - build_action_listener(Editor::toggle_code_actions), - // build_action_listener(Editor::open_excerpts), todo!() - build_action_listener(Editor::toggle_soft_wrap), - build_action_listener(Editor::toggle_inlay_hints), - build_action_listener(Editor::reveal_in_finder), - build_action_listener(Editor::copy_path), - build_action_listener(Editor::copy_relative_path), - build_action_listener(Editor::copy_highlight_json), - build_action_listener(|editor, action, cx| { - editor - .format(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }), - build_action_listener(Editor::restart_language_server), - build_action_listener(Editor::show_character_palette), - // build_action_listener(Editor::confirm_completion), todo!() - build_action_listener(|editor, action, cx| { - editor - .confirm_code_action(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }), - // build_action_listener(Editor::rename), todo!() - // build_action_listener(Editor::confirm_rename), todo!() - // build_action_listener(Editor::find_all_references), todo!() - build_action_listener(Editor::next_copilot_suggestion), - build_action_listener(Editor::previous_copilot_suggestion), - build_action_listener(Editor::copilot_suggest), - build_action_listener(Editor::context_menu_first), - build_action_listener(Editor::context_menu_prev), - build_action_listener(Editor::context_menu_next), - build_action_listener(Editor::context_menu_last), - ] -} - -fn build_key_listener( - listener: impl Fn( - &mut Editor, - &T, - &[&KeyContext], - DispatchPhase, - &mut ViewContext, - ) -> Option> - + 'static, -) -> (TypeId, KeyListener) { - ( - TypeId::of::(), - Box::new(move |editor, event, dispatch_context, phase, cx| { - let key_event = event.downcast_ref::()?; - listener(editor, key_event, dispatch_context, phase, cx) - }), - ) -} - -fn build_action_listener( +fn handle_action( + cx: &mut ViewContext, listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, -) -> (TypeId, KeyListener) { - build_key_listener(move |editor, action: &T, dispatch_context, phase, cx| { +) { + cx.on_action(TypeId::of::(), move |editor, action, phase, cx| { + let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Bubble { listener(editor, action, cx); } - None }) } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index eaac9fc71e..7bfd4b244a 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -234,6 +234,7 @@ where element_state.focus_handle.take(), cx, |focus_handle, cx| { + this.interactivity.initialize(cx); element_state.focus_handle = focus_handle; for child in &mut this.children { child.initialize(view_state, cx); diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 9ac1f56099..4a7633f8dc 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -165,43 +165,40 @@ pub trait StatelessInteractive: Element { } /// Capture the given action, fires during the capture phase - fn capture_action( + fn capture_action( mut self, listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, ) -> Self where Self: Sized, { - self.stateless_interactivity().key_listeners.push(( + self.stateless_interactivity().action_listeners.push(( TypeId::of::(), - Box::new(move |view, action, _dipatch_context, phase, cx| { + Box::new(move |view, action, phase, cx| { let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Capture { listener(view, action, cx) } - None }), )); self } /// Add a listener for the given action, fires during the bubble event phase - fn on_action( + fn on_action( mut self, listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, ) -> Self where Self: Sized, { - self.stateless_interactivity().key_listeners.push(( + self.stateless_interactivity().action_listeners.push(( TypeId::of::(), - Box::new(move |view, action, _dispatch_context, phase, cx| { + Box::new(move |view, action, phase, cx| { let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Bubble { listener(view, action, cx) } - - None }), )); self @@ -214,14 +211,11 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interactivity().key_listeners.push(( - TypeId::of::(), - Box::new(move |view, event, _, phase, cx| { - let event = event.downcast_ref().unwrap(); - listener(view, event, phase, cx); - None - }), - )); + self.stateless_interactivity() + .key_down_listeners + .push(Box::new(move |view, event, phase, cx| { + listener(view, event, phase, cx) + })); self } @@ -232,14 +226,11 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interactivity().key_listeners.push(( - TypeId::of::(), - Box::new(move |view, event, _, phase, cx| { - let event = event.downcast_ref().unwrap(); - listener(view, event, phase, cx); - None - }), - )); + self.stateless_interactivity() + .key_up_listeners + .push(Box::new(move |view, event, phase, cx| { + listener(view, event, phase, cx) + })); self } @@ -439,6 +430,26 @@ pub trait ElementInteractivity: 'static { } } + fn initialize(&mut self, cx: &mut ViewContext) { + let stateless = self.as_stateless_mut(); + + for listener in stateless.key_down_listeners.drain(..) { + cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { + listener(state, event, phase, cx); + }) + } + + for listener in stateless.key_up_listeners.drain(..) { + cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { + listener(state, event, phase, cx); + }) + } + + for (action_type, listener) in stateless.action_listeners.drain(..) { + cx.on_action(action_type, listener) + } + } + fn paint( &mut self, bounds: Bounds, @@ -765,7 +776,9 @@ pub struct StatelessInteractivity { pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, - pub key_listeners: SmallVec<[(TypeId, KeyListener); 32]>, + pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, + pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, + pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, pub hover_style: StyleRefinement, pub group_hover_style: Option, drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, @@ -867,7 +880,9 @@ impl Default for StatelessInteractivity { mouse_up_listeners: SmallVec::new(), mouse_move_listeners: SmallVec::new(), scroll_wheel_listeners: SmallVec::new(), - key_listeners: SmallVec::new(), + key_down_listeners: SmallVec::new(), + key_up_listeners: SmallVec::new(), + action_listeners: SmallVec::new(), hover_style: StyleRefinement::default(), group_hover_style: None, drag_over_styles: SmallVec::new(), @@ -1202,16 +1217,14 @@ pub(crate) type HoverListener = Box) pub(crate) type TooltipBuilder = Arc) -> AnyView + 'static>; -pub type KeyListener = Box< - dyn Fn( - &mut V, - &dyn Any, - &[&KeyContext], - DispatchPhase, - &mut ViewContext, - ) -> Option> - + 'static, ->; +pub(crate) type KeyDownListener = + Box) + 'static>; + +pub(crate) type KeyUpListener = + Box) + 'static>; + +pub type ActionListener = + Box) + 'static>; #[cfg(test)] mod test { diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 9f76df82c3..40d6c66973 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -69,6 +69,7 @@ impl KeyDispatcher { }); self.node_stack.push(node_id); if !context.is_empty() { + self.active_node().context = context.clone(); self.context_stack.push(context); if let Some((context_stack, matcher)) = old_dispatcher .keystroke_matchers @@ -153,6 +154,7 @@ impl KeyDispatcher { // Capture phase self.context_stack.clear(); cx.propagate_event = true; + for node_id in &dispatch_path { let node = &self.nodes[node_id.0]; if !node.context.is_empty() { @@ -193,18 +195,16 @@ impl KeyDispatcher { ); } - if let Some(keystroke_matcher) = self + let keystroke_matcher = self .keystroke_matchers .get_mut(self.context_stack.as_slice()) + .unwrap(); + if let KeyMatch::Some(action) = keystroke_matcher + .match_keystroke(&key_down_event.keystroke, self.context_stack.as_slice()) { - if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke( - &key_down_event.keystroke, - self.context_stack.as_slice(), - ) { - self.dispatch_action_on_node(*node_id, action, cx); - if !cx.propagate_event { - return; - } + self.dispatch_action_on_node(*node_id, action, cx); + if !cx.propagate_event { + return; } } } @@ -236,10 +236,17 @@ impl KeyDispatcher { // Capture phase for node_id in &dispatch_path { let node = &self.nodes[node_id.0]; - for ActionListener { listener, .. } in &node.action_listeners { - listener(&action, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; + for ActionListener { + action_type, + listener, + } in &node.action_listeners + { + let any_action = action.as_any(); + if *action_type == any_action.type_id() { + listener(any_action, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } } } } @@ -247,11 +254,18 @@ impl KeyDispatcher { // Bubble phase for node_id in dispatch_path.iter().rev() { let node = &self.nodes[node_id.0]; - for ActionListener { listener, .. } in &node.action_listeners { - cx.propagate_event = false; // Actions stop propagation by default during the bubble phase - listener(&action, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; + for ActionListener { + action_type, + listener, + } in &node.action_listeners + { + let any_action = action.as_any(); + if *action_type == any_action.type_id() { + cx.propagate_event = false; // Actions stop propagation by default during the bubble phase + listener(any_action, DispatchPhase::Bubble, cx); + if !cx.propagate_event { + return; + } } } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 3da2664f79..82d5982475 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -713,6 +713,42 @@ impl<'a> WindowContext<'a> { )) } + /// Register a key event listener on the window for the current frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a global listener. + pub fn on_key_event( + &mut self, + handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, + ) { + let key_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); + key_dispatcher.on_key_event(Box::new(move |event, phase, cx| { + if let Some(event) = event.downcast_ref::() { + handler(event, phase, cx) + } + })); + } + + /// Register an action listener on the window for the current frame. The type of action + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using action handlers on elements unless you have + /// a specific need to register a global listener. + pub fn on_action( + &mut self, + action_type: TypeId, + handler: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static, + ) { + let key_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); + key_dispatcher.on_action( + action_type, + Box::new(move |action, phase, cx| handler(action, phase, cx)), + ); + } + /// The position of the mouse relative to the window. pub fn mouse_position(&self) -> Point { self.window.mouse_position @@ -1955,6 +1991,32 @@ impl<'a, V: 'static> ViewContext<'a, V> { }); } + pub fn on_key_event( + &mut self, + handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, + ) { + let handle = self.view(); + self.window_cx.on_key_event(move |event, phase, cx| { + handle.update(cx, |view, cx| { + handler(view, event, phase, cx); + }) + }); + } + + pub fn on_action( + &mut self, + action_type: TypeId, + handler: impl Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext) + 'static, + ) { + let handle = self.view(); + self.window_cx + .on_action(action_type, move |action, phase, cx| { + handle.update(cx, |view, cx| { + handler(view, action, phase, cx); + }) + }); + } + /// Set an input handler, such as [ElementInputHandler], which interfaces with the /// platform to receive textual input with proper integration with concerns such /// as IME interactions. diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 368fb20fbf..142e71cde4 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -39,10 +39,10 @@ impl Render for FocusStory { .focusable() .context("parent") .on_action(|_, action: &ActionA, cx| { - println!("Action A dispatched on parent during"); + println!("Action A dispatched on parent"); }) .on_action(|_, action: &ActionB, cx| { - println!("Action B dispatched on parent during"); + println!("Action B dispatched on parent"); }) .on_focus(|_, _, _| println!("Parent focused")) .on_blur(|_, _, _| println!("Parent blurred")) @@ -79,7 +79,7 @@ impl Render for FocusStory { .track_focus(&child_2) .context("child-2") .on_action(|_, action: &ActionC, cx| { - println!("Action C dispatched on child 2 during"); + println!("Action C dispatched on child 2"); }) .w_full() .h_6() From 26d26fadb340aafbf497402d34b0ab79a949a65a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 14:35:49 +0100 Subject: [PATCH 015/126] Fix focus story --- crates/storybook2/src/stories/focus.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 142e71cde4..bba798d9fe 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -1,12 +1,16 @@ use gpui::{ - actions, div, Div, Focusable, FocusableKeyDispatch, KeyBinding, ParentElement, Render, - StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, WindowContext, + actions, div, Div, FocusHandle, Focusable, FocusableKeyDispatch, KeyBinding, ParentElement, + Render, StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, + WindowContext, }; use theme2::ActiveTheme; actions!(ActionA, ActionB, ActionC); -pub struct FocusStory {} +pub struct FocusStory { + child_1_focus: FocusHandle, + child_2_focus: FocusHandle, +} impl FocusStory { pub fn view(cx: &mut WindowContext) -> View { @@ -16,7 +20,10 @@ impl FocusStory { KeyBinding::new("cmd-c", ActionC, None), ]); - cx.build_view(move |cx| Self {}) + cx.build_view(move |cx| Self { + child_1_focus: cx.focus_handle(), + child_2_focus: cx.focus_handle(), + }) } } @@ -31,8 +38,6 @@ impl Render for FocusStory { let color_4 = theme.status().conflict; let color_5 = theme.status().ignored; let color_6 = theme.status().renamed; - let child_1 = cx.focus_handle(); - let child_2 = cx.focus_handle(); div() .id("parent") @@ -56,7 +61,7 @@ impl Render for FocusStory { .focus_in(|style| style.bg(color_3)) .child( div() - .track_focus(&child_1) + .track_focus(&self.child_1_focus) .context("child-1") .on_action(|_, action: &ActionB, cx| { println!("Action B dispatched on child 1 during"); @@ -76,7 +81,7 @@ impl Render for FocusStory { ) .child( div() - .track_focus(&child_2) + .track_focus(&self.child_2_focus) .context("child-2") .on_action(|_, action: &ActionC, cx| { println!("Action C dispatched on child 2"); From 827b16bf5c7ec811aaf719b4870fc1ddd91e3891 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 14:42:16 +0100 Subject: [PATCH 016/126] Capture node in dispatch tree even if it's not focusable --- crates/gpui2/src/key_dispatch.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 40d6c66973..6dac90fc59 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -305,7 +305,7 @@ pub trait KeyDispatch: 'static { cx: &mut ViewContext, f: impl FnOnce(Option, &mut ViewContext) -> R, ) -> R { - if let Some(focusable) = self.as_focusable_mut() { + let focus_handle = if let Some(focusable) = self.as_focusable_mut() { let focus_handle = focusable .focus_handle .get_or_insert_with(|| focus_handle.unwrap_or_else(|| cx.focus_handle())) @@ -316,11 +316,12 @@ pub trait KeyDispatch: 'static { listener(view, &focus_handle, event, cx) }); } - - cx.with_key_dispatch(self.key_context().clone(), Some(focus_handle), f) + Some(focus_handle) } else { - f(None, cx) - } + None + }; + + cx.with_key_dispatch(self.key_context().clone(), focus_handle, f) } fn refine_style(&self, style: &mut Style, cx: &WindowContext) { From d0b5c654aae5ea4cf45660aba9bfae936bab2bba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 14:48:08 +0100 Subject: [PATCH 017/126] Clear pending keystrokes when finding action --- crates/gpui2/src/key_dispatch.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 6dac90fc59..e44dc51c05 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -202,6 +202,11 @@ impl KeyDispatcher { if let KeyMatch::Some(action) = keystroke_matcher .match_keystroke(&key_down_event.keystroke, self.context_stack.as_slice()) { + // Clear all pending keystrokes when an action has been found. + for keystroke_matcher in self.keystroke_matchers.values_mut() { + keystroke_matcher.clear_pending(); + } + self.dispatch_action_on_node(*node_id, action, cx); if !cx.propagate_event { return; From c8fb8e2859eb5b9476c6489a2a0299f269f75a1f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 15:20:43 +0100 Subject: [PATCH 018/126] :lipstick: --- crates/editor2/src/element.rs | 252 +++++++++++++++++----------------- 1 file changed, 126 insertions(+), 126 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index b273c5914a..1b0f3c473c 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2460,150 +2460,150 @@ impl Element for EditorElement { dispatch_context, Some(editor.focus_handle.clone()), |_, cx| { - handle_action(cx, Editor::move_left); - handle_action(cx, Editor::move_right); - handle_action(cx, Editor::move_down); - handle_action(cx, Editor::move_up); + register_action(cx, Editor::move_left); + register_action(cx, Editor::move_right); + register_action(cx, Editor::move_down); + register_action(cx, Editor::move_up); // on_action(cx, Editor::new_file); todo!() // on_action(cx, Editor::new_file_in_direction); todo!() - handle_action(cx, Editor::cancel); - handle_action(cx, Editor::newline); - handle_action(cx, Editor::newline_above); - handle_action(cx, Editor::newline_below); - handle_action(cx, Editor::backspace); - handle_action(cx, Editor::delete); - handle_action(cx, Editor::tab); - handle_action(cx, Editor::tab_prev); - handle_action(cx, Editor::indent); - handle_action(cx, Editor::outdent); - handle_action(cx, Editor::delete_line); - handle_action(cx, Editor::join_lines); - handle_action(cx, Editor::sort_lines_case_sensitive); - handle_action(cx, Editor::sort_lines_case_insensitive); - handle_action(cx, Editor::reverse_lines); - handle_action(cx, Editor::shuffle_lines); - handle_action(cx, Editor::convert_to_upper_case); - handle_action(cx, Editor::convert_to_lower_case); - handle_action(cx, Editor::convert_to_title_case); - handle_action(cx, Editor::convert_to_snake_case); - handle_action(cx, Editor::convert_to_kebab_case); - handle_action(cx, Editor::convert_to_upper_camel_case); - handle_action(cx, Editor::convert_to_lower_camel_case); - handle_action(cx, Editor::delete_to_previous_word_start); - handle_action(cx, Editor::delete_to_previous_subword_start); - handle_action(cx, Editor::delete_to_next_word_end); - handle_action(cx, Editor::delete_to_next_subword_end); - handle_action(cx, Editor::delete_to_beginning_of_line); - handle_action(cx, Editor::delete_to_end_of_line); - handle_action(cx, Editor::cut_to_end_of_line); - handle_action(cx, Editor::duplicate_line); - handle_action(cx, Editor::move_line_up); - handle_action(cx, Editor::move_line_down); - handle_action(cx, Editor::transpose); - handle_action(cx, Editor::cut); - handle_action(cx, Editor::copy); - handle_action(cx, Editor::paste); - handle_action(cx, Editor::undo); - handle_action(cx, Editor::redo); - handle_action(cx, Editor::move_page_up); - handle_action(cx, Editor::move_page_down); - handle_action(cx, Editor::next_screen); - handle_action(cx, Editor::scroll_cursor_top); - handle_action(cx, Editor::scroll_cursor_center); - handle_action(cx, Editor::scroll_cursor_bottom); - handle_action(cx, |editor, _: &LineDown, cx| { + register_action(cx, Editor::cancel); + register_action(cx, Editor::newline); + register_action(cx, Editor::newline_above); + register_action(cx, Editor::newline_below); + register_action(cx, Editor::backspace); + register_action(cx, Editor::delete); + register_action(cx, Editor::tab); + register_action(cx, Editor::tab_prev); + register_action(cx, Editor::indent); + register_action(cx, Editor::outdent); + register_action(cx, Editor::delete_line); + register_action(cx, Editor::join_lines); + register_action(cx, Editor::sort_lines_case_sensitive); + register_action(cx, Editor::sort_lines_case_insensitive); + register_action(cx, Editor::reverse_lines); + register_action(cx, Editor::shuffle_lines); + register_action(cx, Editor::convert_to_upper_case); + register_action(cx, Editor::convert_to_lower_case); + register_action(cx, Editor::convert_to_title_case); + register_action(cx, Editor::convert_to_snake_case); + register_action(cx, Editor::convert_to_kebab_case); + register_action(cx, Editor::convert_to_upper_camel_case); + register_action(cx, Editor::convert_to_lower_camel_case); + register_action(cx, Editor::delete_to_previous_word_start); + register_action(cx, Editor::delete_to_previous_subword_start); + register_action(cx, Editor::delete_to_next_word_end); + register_action(cx, Editor::delete_to_next_subword_end); + register_action(cx, Editor::delete_to_beginning_of_line); + register_action(cx, Editor::delete_to_end_of_line); + register_action(cx, Editor::cut_to_end_of_line); + register_action(cx, Editor::duplicate_line); + register_action(cx, Editor::move_line_up); + register_action(cx, Editor::move_line_down); + register_action(cx, Editor::transpose); + register_action(cx, Editor::cut); + register_action(cx, Editor::copy); + register_action(cx, Editor::paste); + register_action(cx, Editor::undo); + register_action(cx, Editor::redo); + register_action(cx, Editor::move_page_up); + register_action(cx, Editor::move_page_down); + register_action(cx, Editor::next_screen); + register_action(cx, Editor::scroll_cursor_top); + register_action(cx, Editor::scroll_cursor_center); + register_action(cx, Editor::scroll_cursor_bottom); + register_action(cx, |editor, _: &LineDown, cx| { editor.scroll_screen(&ScrollAmount::Line(1.), cx) }); - handle_action(cx, |editor, _: &LineUp, cx| { + register_action(cx, |editor, _: &LineUp, cx| { editor.scroll_screen(&ScrollAmount::Line(-1.), cx) }); - handle_action(cx, |editor, _: &HalfPageDown, cx| { + register_action(cx, |editor, _: &HalfPageDown, cx| { editor.scroll_screen(&ScrollAmount::Page(0.5), cx) }); - handle_action(cx, |editor, _: &HalfPageUp, cx| { + register_action(cx, |editor, _: &HalfPageUp, cx| { editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) }); - handle_action(cx, |editor, _: &PageDown, cx| { + register_action(cx, |editor, _: &PageDown, cx| { editor.scroll_screen(&ScrollAmount::Page(1.), cx) }); - handle_action(cx, |editor, _: &PageUp, cx| { + register_action(cx, |editor, _: &PageUp, cx| { editor.scroll_screen(&ScrollAmount::Page(-1.), cx) }); - handle_action(cx, Editor::move_to_previous_word_start); - handle_action(cx, Editor::move_to_previous_subword_start); - handle_action(cx, Editor::move_to_next_word_end); - handle_action(cx, Editor::move_to_next_subword_end); - handle_action(cx, Editor::move_to_beginning_of_line); - handle_action(cx, Editor::move_to_end_of_line); - handle_action(cx, Editor::move_to_start_of_paragraph); - handle_action(cx, Editor::move_to_end_of_paragraph); - handle_action(cx, Editor::move_to_beginning); - handle_action(cx, Editor::move_to_end); - handle_action(cx, Editor::select_up); - handle_action(cx, Editor::select_down); - handle_action(cx, Editor::select_left); - handle_action(cx, Editor::select_right); - handle_action(cx, Editor::select_to_previous_word_start); - handle_action(cx, Editor::select_to_previous_subword_start); - handle_action(cx, Editor::select_to_next_word_end); - handle_action(cx, Editor::select_to_next_subword_end); - handle_action(cx, Editor::select_to_beginning_of_line); - handle_action(cx, Editor::select_to_end_of_line); - handle_action(cx, Editor::select_to_start_of_paragraph); - handle_action(cx, Editor::select_to_end_of_paragraph); - handle_action(cx, Editor::select_to_beginning); - handle_action(cx, Editor::select_to_end); - handle_action(cx, Editor::select_all); - handle_action(cx, |editor, action, cx| { + register_action(cx, Editor::move_to_previous_word_start); + register_action(cx, Editor::move_to_previous_subword_start); + register_action(cx, Editor::move_to_next_word_end); + register_action(cx, Editor::move_to_next_subword_end); + register_action(cx, Editor::move_to_beginning_of_line); + register_action(cx, Editor::move_to_end_of_line); + register_action(cx, Editor::move_to_start_of_paragraph); + register_action(cx, Editor::move_to_end_of_paragraph); + register_action(cx, Editor::move_to_beginning); + register_action(cx, Editor::move_to_end); + register_action(cx, Editor::select_up); + register_action(cx, Editor::select_down); + register_action(cx, Editor::select_left); + register_action(cx, Editor::select_right); + register_action(cx, Editor::select_to_previous_word_start); + register_action(cx, Editor::select_to_previous_subword_start); + register_action(cx, Editor::select_to_next_word_end); + register_action(cx, Editor::select_to_next_subword_end); + register_action(cx, Editor::select_to_beginning_of_line); + register_action(cx, Editor::select_to_end_of_line); + register_action(cx, Editor::select_to_start_of_paragraph); + register_action(cx, Editor::select_to_end_of_paragraph); + register_action(cx, Editor::select_to_beginning); + register_action(cx, Editor::select_to_end); + register_action(cx, Editor::select_all); + register_action(cx, |editor, action, cx| { editor.select_all_matches(action, cx).log_err(); }); - handle_action(cx, Editor::select_line); - handle_action(cx, Editor::split_selection_into_lines); - handle_action(cx, Editor::add_selection_above); - handle_action(cx, Editor::add_selection_below); - handle_action(cx, |editor, action, cx| { + register_action(cx, Editor::select_line); + register_action(cx, Editor::split_selection_into_lines); + register_action(cx, Editor::add_selection_above); + register_action(cx, Editor::add_selection_below); + register_action(cx, |editor, action, cx| { editor.select_next(action, cx).log_err(); }); - handle_action(cx, |editor, action, cx| { + register_action(cx, |editor, action, cx| { editor.select_previous(action, cx).log_err(); }); - handle_action(cx, Editor::toggle_comments); - handle_action(cx, Editor::select_larger_syntax_node); - handle_action(cx, Editor::select_smaller_syntax_node); - handle_action(cx, Editor::move_to_enclosing_bracket); - handle_action(cx, Editor::undo_selection); - handle_action(cx, Editor::redo_selection); - handle_action(cx, Editor::go_to_diagnostic); - handle_action(cx, Editor::go_to_prev_diagnostic); - handle_action(cx, Editor::go_to_hunk); - handle_action(cx, Editor::go_to_prev_hunk); - handle_action(cx, Editor::go_to_definition); - handle_action(cx, Editor::go_to_definition_split); - handle_action(cx, Editor::go_to_type_definition); - handle_action(cx, Editor::go_to_type_definition_split); - handle_action(cx, Editor::fold); - handle_action(cx, Editor::fold_at); - handle_action(cx, Editor::unfold_lines); - handle_action(cx, Editor::unfold_at); - handle_action(cx, Editor::fold_selected_ranges); - handle_action(cx, Editor::show_completions); - handle_action(cx, Editor::toggle_code_actions); + register_action(cx, Editor::toggle_comments); + register_action(cx, Editor::select_larger_syntax_node); + register_action(cx, Editor::select_smaller_syntax_node); + register_action(cx, Editor::move_to_enclosing_bracket); + register_action(cx, Editor::undo_selection); + register_action(cx, Editor::redo_selection); + register_action(cx, Editor::go_to_diagnostic); + register_action(cx, Editor::go_to_prev_diagnostic); + register_action(cx, Editor::go_to_hunk); + register_action(cx, Editor::go_to_prev_hunk); + register_action(cx, Editor::go_to_definition); + register_action(cx, Editor::go_to_definition_split); + register_action(cx, Editor::go_to_type_definition); + register_action(cx, Editor::go_to_type_definition_split); + register_action(cx, Editor::fold); + register_action(cx, Editor::fold_at); + register_action(cx, Editor::unfold_lines); + register_action(cx, Editor::unfold_at); + register_action(cx, Editor::fold_selected_ranges); + register_action(cx, Editor::show_completions); + register_action(cx, Editor::toggle_code_actions); // on_action(cx, Editor::open_excerpts); todo!() - handle_action(cx, Editor::toggle_soft_wrap); - handle_action(cx, Editor::toggle_inlay_hints); - handle_action(cx, Editor::reveal_in_finder); - handle_action(cx, Editor::copy_path); - handle_action(cx, Editor::copy_relative_path); - handle_action(cx, Editor::copy_highlight_json); - handle_action(cx, |editor, action, cx| { + register_action(cx, Editor::toggle_soft_wrap); + register_action(cx, Editor::toggle_inlay_hints); + register_action(cx, Editor::reveal_in_finder); + register_action(cx, Editor::copy_path); + register_action(cx, Editor::copy_relative_path); + register_action(cx, Editor::copy_highlight_json); + register_action(cx, |editor, action, cx| { editor .format(action, cx) .map(|task| task.detach_and_log_err(cx)); }); - handle_action(cx, Editor::restart_language_server); - handle_action(cx, Editor::show_character_palette); + register_action(cx, Editor::restart_language_server); + register_action(cx, Editor::show_character_palette); // on_action(cx, Editor::confirm_completion); todo!() - handle_action(cx, |editor, action, cx| { + register_action(cx, |editor, action, cx| { editor .confirm_code_action(action, cx) .map(|task| task.detach_and_log_err(cx)); @@ -2611,13 +2611,13 @@ impl Element for EditorElement { // on_action(cx, Editor::rename); todo!() // on_action(cx, Editor::confirm_rename); todo!() // on_action(cx, Editor::find_all_references); todo!() - handle_action(cx, Editor::next_copilot_suggestion); - handle_action(cx, Editor::previous_copilot_suggestion); - handle_action(cx, Editor::copilot_suggest); - handle_action(cx, Editor::context_menu_first); - handle_action(cx, Editor::context_menu_prev); - handle_action(cx, Editor::context_menu_next); - handle_action(cx, Editor::context_menu_last); + register_action(cx, Editor::next_copilot_suggestion); + register_action(cx, Editor::previous_copilot_suggestion); + register_action(cx, Editor::copilot_suggest); + register_action(cx, Editor::context_menu_first); + register_action(cx, Editor::context_menu_prev); + register_action(cx, Editor::context_menu_next); + register_action(cx, Editor::context_menu_last); }, ) }); @@ -4154,7 +4154,7 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { // } // } -fn handle_action( +fn register_action( cx: &mut ViewContext, listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, ) { From 44534b926d51ff577f6a19b95aab6f90438389ac Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 15:21:47 +0100 Subject: [PATCH 019/126] Register actions on the right div --- crates/gpui2/src/keymap/context.rs | 20 +++++++++++++++++++- crates/workspace2/src/workspace2.rs | 15 ++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/gpui2/src/keymap/context.rs b/crates/gpui2/src/keymap/context.rs index b0225e73e7..99a95531a2 100644 --- a/crates/gpui2/src/keymap/context.rs +++ b/crates/gpui2/src/keymap/context.rs @@ -1,8 +1,9 @@ use crate::SharedString; use anyhow::{anyhow, Result}; use smallvec::SmallVec; +use std::fmt; -#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)] +#[derive(Clone, Default, Eq, PartialEq, Hash)] pub struct KeyContext(SmallVec<[ContextEntry; 8]>); #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -99,6 +100,23 @@ impl KeyContext { } } +impl fmt::Debug for KeyContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut entries = self.0.iter().peekable(); + while let Some(entry) = entries.next() { + if let Some(ref value) = entry.value { + write!(f, "{}={}", entry.key, value)?; + } else { + write!(f, "{}", entry.key)?; + } + if entries.peek().is_some() { + write!(f, " ")?; + } + } + Ok(()) + } +} + #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum KeyBindingContextPredicate { Identifier(SharedString), diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index e55d59303d..21c72fd385 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -39,7 +39,7 @@ use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, - StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription, Task, + StatefulInteractive, StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; @@ -534,8 +534,8 @@ pub struct Workspace { workspace_actions: Vec< Box< dyn Fn( - Div>, - ) -> Div>, + Div>, + ) -> Div>, >, >, zoomed: Option, @@ -3514,8 +3514,8 @@ impl Workspace { fn add_workspace_actions_listeners( &self, - mut div: Div>, - ) -> Div> { + mut div: Div>, + ) -> Div> { for action in self.workspace_actions.iter() { div = (action)(div) } @@ -3746,7 +3746,7 @@ impl Render for Workspace { let mut context = KeyContext::default(); context.add("Workspace"); - div() + self.add_workspace_actions_listeners(div()) .context(context) .relative() .size_full() @@ -3761,7 +3761,8 @@ impl Render for Workspace { .child(self.render_titlebar(cx)) .child( // todo! should this be a component a view? - self.add_workspace_actions_listeners(div().id("workspace")) + div() + .id("workspace") .relative() .flex_1() .w_full() From 45fef27aa1807f54f9a0602b0e8d47abffb3ab4c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 15:31:35 +0100 Subject: [PATCH 020/126] Clear all the state when clearing KeyDispatcher --- crates/gpui2/src/key_dispatch.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index e44dc51c05..b0f4a5d8d2 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -58,6 +58,9 @@ impl KeyDispatcher { pub fn clear(&mut self) { self.node_stack.clear(); self.nodes.clear(); + self.context_stack.clear(); + self.focusable_node_ids.clear(); + self.keystroke_matchers.clear(); } pub fn push_node(&mut self, context: KeyContext, old_dispatcher: &mut Self) { From be8bd437cdd09a2cc005bc2f2b3c95b8e5c71c3b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 13 Nov 2023 10:41:56 -0500 Subject: [PATCH 021/126] Update jetbrains keymap to match community repo --- assets/keymaps/jetbrains.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index ab093a8deb..b2ed144a3f 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -10,6 +10,7 @@ "bindings": { "ctrl->": "zed::IncreaseBufferFontSize", "ctrl-<": "zed::DecreaseBufferFontSize", + "ctrl-shift-j": "editor::JoinLines", "cmd-d": "editor::DuplicateLine", "cmd-backspace": "editor::DeleteLine", "cmd-pagedown": "editor::MovePageDown", @@ -18,7 +19,7 @@ "cmd-alt-enter": "editor::NewlineAbove", "shift-enter": "editor::NewlineBelow", "cmd--": "editor::Fold", - "cmd-=": "editor::UnfoldLines", + "cmd-+": "editor::UnfoldLines", "alt-shift-g": "editor::SplitSelectionIntoLines", "ctrl-g": [ "editor::SelectNext", From 5b254b03df3e3d108f0cfead80b0d46087eec895 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 13 Nov 2023 11:10:00 -0500 Subject: [PATCH 022/126] Move `Sized` bound to `StyledExt` trait --- crates/ui2/src/styled_ext.rs | 42 ++++++++---------------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index 06352fa44b..3d6af476a4 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -12,31 +12,22 @@ fn elevated(this: E, cx: &mut ViewContext, index: Elev } /// Extends [`Styled`](gpui::Styled) with Zed specific styling methods. -pub trait StyledExt: Styled { +pub trait StyledExt: Styled + Sized { /// Horizontally stacks elements. /// /// Sets `flex()`, `flex_row()`, `items_center()` - fn h_flex(self) -> Self - where - Self: Sized, - { + fn h_flex(self) -> Self { self.flex().flex_row().items_center() } /// Vertically stacks elements. /// /// Sets `flex()`, `flex_col()` - fn v_flex(self) -> Self - where - Self: Sized, - { + fn v_flex(self) -> Self { self.flex().flex_col() } - fn text_ui_size(self, size: UITextSize) -> Self - where - Self: Sized, - { + fn text_ui_size(self, size: UITextSize) -> Self { let size = size.rems(); self.text_size(size) @@ -49,10 +40,7 @@ pub trait StyledExt: Styled { /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. /// /// Use [`text_ui_sm`] for regular-sized text. - fn text_ui(self) -> Self - where - Self: Sized, - { + fn text_ui(self) -> Self { let size = UITextSize::default().rems(); self.text_size(size) @@ -65,10 +53,7 @@ pub trait StyledExt: Styled { /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. /// /// Use [`text_ui`] for regular-sized text. - fn text_ui_sm(self) -> Self - where - Self: Sized, - { + fn text_ui_sm(self) -> Self { let size = UITextSize::Small.rems(); self.text_size(size) @@ -79,10 +64,7 @@ pub trait StyledExt: Styled { /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// /// Example Elements: Title Bar, Panel, Tab Bar, Editor - fn elevation_1(self, cx: &mut ViewContext) -> Self - where - Self: Styled + Sized, - { + fn elevation_1(self, cx: &mut ViewContext) -> Self { elevated(self, cx, ElevationIndex::Surface) } @@ -91,10 +73,7 @@ pub trait StyledExt: Styled { /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// /// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels - fn elevation_2(self, cx: &mut ViewContext) -> Self - where - Self: Styled + Sized, - { + fn elevation_2(self, cx: &mut ViewContext) -> Self { elevated(self, cx, ElevationIndex::ElevatedSurface) } @@ -109,10 +88,7 @@ pub trait StyledExt: Styled { /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// /// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs - fn elevation_4(self, cx: &mut ViewContext) -> Self - where - Self: Styled + Sized, - { + fn elevation_4(self, cx: &mut ViewContext) -> Self { elevated(self, cx, ElevationIndex::ModalSurface) } } From 3654dd8da011256994c63fb97980ce1b735d23b3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 13 Nov 2023 11:10:08 -0500 Subject: [PATCH 023/126] Remove unnecessary `map` --- crates/ui2/src/components/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 57143e1f0c..5c42975b17 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -401,7 +401,7 @@ impl List { v_stack() .w_full() .py_1() - .children(self.header.map(|header| header)) + .children(self.header) .child(list_content) } } From dbd26ac6510704ec819d51f0ab79ea721d503ff6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 13 Nov 2023 17:48:46 +0200 Subject: [PATCH 024/126] Make inlay hint cache tests pass Co-Authored-By: Conrad --- crates/editor2/src/inlay_hint_cache.rs | 46 ++++----------- crates/editor2/src/scroll/scroll_amount.rs | 27 ++++----- crates/gpui2/src/platform/test/window.rs | 69 ++++++++++++++++++---- 3 files changed, 82 insertions(+), 60 deletions(-) diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index 8beee2ba9a..af9febf376 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -1220,8 +1220,6 @@ pub mod tests { use super::*; - // todo!() - #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"] #[gpui::test] async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); @@ -1345,8 +1343,6 @@ pub mod tests { }); } - // todo!() - #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"] #[gpui::test] async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { @@ -1458,8 +1454,6 @@ pub mod tests { }); } - // todo!() - #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"] #[gpui::test] async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { @@ -1668,8 +1662,6 @@ pub mod tests { }); } - // todo!() - #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"] #[gpui::test] async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); @@ -1998,8 +1990,6 @@ pub mod tests { }); } - // todo!() - #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"] #[gpui::test] async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { @@ -2126,8 +2116,6 @@ pub mod tests { }); } - // todo!() - #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"] #[gpui::test(iterations = 10)] async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { @@ -2411,8 +2399,6 @@ pub mod tests { }); } - // todo!() - #[ignore = "fails due to text.rs `measurement has not been performed` error"] #[gpui::test(iterations = 10)] async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { @@ -2455,14 +2441,9 @@ pub mod tests { project.update(cx, |project, _| { project.languages().add(Arc::clone(&language)) }); - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let worktree_id = workspace - .update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.worktrees().next().unwrap().read(cx).id() - }) - }) - .unwrap(); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); let buffer_1 = project .update(cx, |project, cx| { @@ -2620,6 +2601,10 @@ pub mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), + // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther + // (or renders less?) note that tests below pass + "main hint #4".to_string(), + "main hint #5".to_string(), ]; assert_eq!( expected_hints, @@ -2755,8 +2740,6 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); } - // todo!() - #[ignore = "fails due to text.rs `measurement has not been performed` error"] #[gpui::test] async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { @@ -2799,14 +2782,9 @@ all hints should be invalidated and requeried for all of its visible excerpts" project.update(cx, |project, _| { project.languages().add(Arc::clone(&language)) }); - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let worktree_id = workspace - .update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.worktrees().next().unwrap().read(cx).id() - }) - }) - .unwrap(); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); let buffer_1 = project .update(cx, |project, cx| { @@ -2985,8 +2963,6 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); } - // todo!() - #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"] #[gpui::test] async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { @@ -3078,8 +3054,6 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); } - // todo!() - #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"] #[gpui::test] async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { diff --git a/crates/editor2/src/scroll/scroll_amount.rs b/crates/editor2/src/scroll/scroll_amount.rs index 89d188e324..2cb22d1516 100644 --- a/crates/editor2/src/scroll/scroll_amount.rs +++ b/crates/editor2/src/scroll/scroll_amount.rs @@ -11,19 +11,18 @@ pub enum ScrollAmount { impl ScrollAmount { pub fn lines(&self, editor: &mut Editor) -> f32 { - todo!() - // match self { - // Self::Line(count) => *count, - // Self::Page(count) => editor - // .visible_line_count() - // .map(|mut l| { - // // for full pages subtract one to leave an anchor line - // if count.abs() == 1.0 { - // l -= 1.0 - // } - // (l * count).trunc() - // }) - // .unwrap_or(0.), - // } + match self { + Self::Line(count) => *count, + Self::Page(count) => editor + .visible_line_count() + .map(|mut l| { + // for full pages subtract one to leave an anchor line + if count.abs() == 1.0 { + l -= 1.0 + } + (l * count).trunc() + }) + .unwrap_or(0.), + } } } diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index f132719655..289ecf7e6b 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -1,10 +1,14 @@ -use std::{rc::Rc, sync::Arc}; +use std::{ + rc::Rc, + sync::{self, Arc}, +}; +use collections::HashMap; use parking_lot::Mutex; use crate::{ - px, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, Scene, Size, - WindowAppearance, WindowBounds, WindowOptions, + px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, + PlatformWindow, Point, Scene, Size, TileId, WindowAppearance, WindowBounds, WindowOptions, }; #[derive(Default)] @@ -30,7 +34,7 @@ impl TestWindow { current_scene: Default::default(), display, - sprite_atlas: Arc::new(TestAtlas), + sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), } } @@ -154,26 +158,71 @@ impl PlatformWindow for TestWindow { self.current_scene.lock().replace(scene); } - fn sprite_atlas(&self) -> std::sync::Arc { + fn sprite_atlas(&self) -> sync::Arc { self.sprite_atlas.clone() } } -pub struct TestAtlas; +pub struct TestAtlasState { + next_id: u32, + tiles: HashMap, +} + +pub struct TestAtlas(Mutex); + +impl TestAtlas { + pub fn new() -> Self { + TestAtlas(Mutex::new(TestAtlasState { + next_id: 0, + tiles: HashMap::default(), + })) + } +} impl PlatformAtlas for TestAtlas { fn get_or_insert_with<'a>( &self, - _key: &crate::AtlasKey, - _build: &mut dyn FnMut() -> anyhow::Result<( + key: &crate::AtlasKey, + build: &mut dyn FnMut() -> anyhow::Result<( Size, std::borrow::Cow<'a, [u8]>, )>, ) -> anyhow::Result { - todo!() + let mut state = self.0.lock(); + if let Some(tile) = state.tiles.get(key) { + return Ok(tile.clone()); + } + + state.next_id += 1; + let texture_id = state.next_id; + state.next_id += 1; + let tile_id = state.next_id; + + drop(state); + let (size, _) = build()?; + let mut state = self.0.lock(); + + state.tiles.insert( + key.clone(), + crate::AtlasTile { + texture_id: AtlasTextureId { + index: texture_id, + kind: crate::AtlasTextureKind::Path, + }, + tile_id: TileId(tile_id), + bounds: crate::Bounds { + origin: Point::zero(), + size, + }, + }, + ); + + Ok(state.tiles[key].clone()) } fn clear(&self) { - todo!() + let mut state = self.0.lock(); + state.tiles = HashMap::default(); + state.next_id = 0; } } From 521972ed9e9bfc7fa8e1cc4e89c2393da0f8d3f3 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 11:56:42 -0500 Subject: [PATCH 025/126] Update Status Colors --- crates/theme2/src/colors.rs | 31 +++++++++++++++++++++++++++++ crates/theme2/src/default_colors.rs | 3 +++ 2 files changed, 34 insertions(+) diff --git a/crates/theme2/src/colors.rs b/crates/theme2/src/colors.rs index b8cceebea8..aab672ad57 100644 --- a/crates/theme2/src/colors.rs +++ b/crates/theme2/src/colors.rs @@ -14,16 +14,47 @@ pub struct SystemColors { #[derive(Refineable, Clone, Debug)] #[refineable(Debug, serde::Deserialize)] pub struct StatusColors { + /// Indicates some kind of conflict, like a file changed on disk while it was open, or + /// merge conflicts in a Git repository. pub conflict: Hsla, + + /// Indicates something new, like a new file added to a Git repository. pub created: Hsla, + + /// Indicates that something no longer exists, like a deleted file. pub deleted: Hsla, + + /// Indicates a system error, a failed operation or a diagnostic error. pub error: Hsla, + + /// Represents a hidden status, such as a file being hidden in a file tree. pub hidden: Hsla, + + /// Indicates a hint or some kind of additional information. + pub hint: Hsla, + + /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. pub ignored: Hsla, + + /// Represents informational status updates or messages. pub info: Hsla, + + /// Indicates a changed or altered status, like a file that has been edited. pub modified: Hsla, + + /// Indicates something that is predicted, like automatic code completion, or generated code. + pub predictive: Hsla, + + /// Represents a renamed status, such as a file that has been renamed. pub renamed: Hsla, + + /// Indicates a successful operation or task completion. pub success: Hsla, + + /// Indicates some kind of unreachable status, like a block of code that can never be reached. + pub unreachable: Hsla, + + /// Represents a warning status, like an operation that is about to fail. pub warning: Hsla, } diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 6cfda37a2a..3e913905f3 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -122,11 +122,14 @@ impl Default for StatusColors { deleted: red().dark().step_9(), error: red().dark().step_9(), hidden: neutral().dark().step_9(), + hint: blue().dark().step_9(), ignored: neutral().dark().step_9(), info: blue().dark().step_9(), modified: yellow().dark().step_9(), + predictive: neutral().dark_alpha().step_9(), renamed: blue().dark().step_9(), success: grass().dark().step_9(), + unreachable: neutral().dark().step_10(), warning: yellow().dark().step_9(), } } From 5361a499aedf84b51c591a5d57eeede3c48d9c84 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 12:09:31 -0500 Subject: [PATCH 026/126] Checkpoint --- crates/theme2/src/default_colors.rs | 21 --------------- crates/theme2/src/default_theme.rs | 4 +-- crates/theme2/src/registry.rs | 2 +- crates/theme2/src/styles.rs | 3 +++ crates/theme2/src/styles/status.rs | 41 +++++++++++++++++++++++++++++ crates/theme2/src/theme2.rs | 2 ++ 6 files changed, 49 insertions(+), 24 deletions(-) create mode 100644 crates/theme2/src/styles.rs create mode 100644 crates/theme2/src/styles/status.rs diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 3e913905f3..43d6259e52 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -114,27 +114,6 @@ impl Default for SystemColors { } } -impl Default for StatusColors { - fn default() -> Self { - Self { - conflict: red().dark().step_9(), - created: grass().dark().step_9(), - deleted: red().dark().step_9(), - error: red().dark().step_9(), - hidden: neutral().dark().step_9(), - hint: blue().dark().step_9(), - ignored: neutral().dark().step_9(), - info: blue().dark().step_9(), - modified: yellow().dark().step_9(), - predictive: neutral().dark_alpha().step_9(), - renamed: blue().dark().step_9(), - success: grass().dark().step_9(), - unreachable: neutral().dark().step_10(), - warning: yellow().dark().step_9(), - } - } -} - impl SyntaxTheme { pub fn default_light() -> Self { Self { diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 40fb7df7cf..4e8caf67b1 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -13,7 +13,7 @@ fn zed_pro_daylight() -> Theme { styles: ThemeStyles { system: SystemColors::default(), colors: ThemeColors::default_light(), - status: StatusColors::default(), + status: StatusColors::light(), player: PlayerColors::default_light(), syntax: Arc::new(SyntaxTheme::default_light()), }, @@ -28,7 +28,7 @@ pub(crate) fn zed_pro_moonlight() -> Theme { styles: ThemeStyles { system: SystemColors::default(), colors: ThemeColors::default_dark(), - status: StatusColors::default(), + status: StatusColors::dark(), player: PlayerColors::default(), syntax: Arc::new(SyntaxTheme::default_dark()), }, diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 0c61f6f224..a28c59b6e1 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -43,7 +43,7 @@ impl ThemeRegistry { }; theme_colors.refine(&user_theme.styles.colors); - let mut status_colors = StatusColors::default(); + let mut status_colors = StatusColors::dark(); status_colors.refine(&user_theme.styles.status); let mut syntax_colors = match user_theme.appearance { diff --git a/crates/theme2/src/styles.rs b/crates/theme2/src/styles.rs new file mode 100644 index 0000000000..0a44e2a468 --- /dev/null +++ b/crates/theme2/src/styles.rs @@ -0,0 +1,3 @@ +mod status; + +use status::*; diff --git a/crates/theme2/src/styles/status.rs b/crates/theme2/src/styles/status.rs new file mode 100644 index 0000000000..87f9da96ee --- /dev/null +++ b/crates/theme2/src/styles/status.rs @@ -0,0 +1,41 @@ +use crate::StatusColors; + +impl StatusColors { + pub fn dark() -> Self { + Self { + conflict: red().dark().step_9(), + created: grass().dark().step_9(), + deleted: red().dark().step_9(), + error: red().dark().step_9(), + hidden: neutral().dark().step_9(), + hint: blue().dark().step_9(), + ignored: neutral().dark().step_9(), + info: blue().dark().step_9(), + modified: yellow().dark().step_9(), + predictive: neutral().dark_alpha().step_9(), + renamed: blue().dark().step_9(), + success: grass().dark().step_9(), + unreachable: neutral().dark().step_10(), + warning: yellow().dark().step_9(), + } + } + + pub fn light() -> Self { + Self { + conflict: red().light().step_9(), + created: grass().light().step_9(), + deleted: red().light().step_9(), + error: red().light().step_9(), + hidden: neutral().light().step_9(), + hint: blue().light().step_9(), + ignored: neutral().light().step_9(), + info: blue().light().step_9(), + modified: yellow().light().step_9(), + predictive: neutral().light_alpha().step_9(), + renamed: blue().light().step_9(), + success: grass().light().step_9(), + unreachable: neutral().light().step_10(), + warning: yellow().light().step_9(), + } + } +} diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index 7e2085de4e..0d6600eca6 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -5,6 +5,7 @@ mod players; mod registry; mod scale; mod settings; +mod styles; mod syntax; #[cfg(not(feature = "importing-themes"))] mod themes; @@ -20,6 +21,7 @@ pub use players::*; pub use registry::*; pub use scale::*; pub use settings::*; +pub use styles::*; pub use syntax::*; #[cfg(not(feature = "importing-themes"))] pub use themes::*; From a6c95ad331899bc0350f19923090982203e97998 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 18:29:18 +0100 Subject: [PATCH 027/126] Fix panic when querying available actions --- crates/editor2/src/element.rs | 12 +- crates/gpui2/src/key_dispatch.rs | 189 +++++---------------- crates/gpui2/src/window.rs | 281 ++++++++++++++++++++++--------- 3 files changed, 250 insertions(+), 232 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 1b0f3c473c..f8386ee271 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -15,12 +15,12 @@ use crate::{ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ - black, hsla, point, px, relative, size, transparent_black, Action, ActionListener, AnyElement, - AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, - Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, - InputHandler, KeyContext, KeyDownEvent, KeyMatch, Line, LineLayout, Modifiers, MouseButton, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size, - Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout, + black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, + BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, + ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, + KeyContext, KeyDownEvent, KeyMatch, Line, LineLayout, Modifiers, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size, Style, TextRun, + TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index b0f4a5d8d2..bc8e1f8f85 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -1,6 +1,6 @@ use crate::{ build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, - FocusId, KeyContext, KeyDownEvent, KeyMatch, Keymap, KeystrokeMatcher, MouseDownEvent, Pixels, + FocusId, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, Pixels, Style, StyleRefinement, ViewContext, WindowContext, }; use collections::HashMap; @@ -9,11 +9,11 @@ use refineable::Refineable; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, + rc::Rc, sync::Arc, }; use util::ResultExt; -type KeyListener = Box; pub type FocusListeners = SmallVec<[FocusListener; 2]>; pub type FocusListener = Box) + 'static>; @@ -21,7 +21,7 @@ pub type FocusListener = #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct DispatchNodeId(usize); -pub struct KeyDispatcher { +pub(crate) struct DispatchTree { node_stack: Vec, context_stack: Vec, nodes: Vec, @@ -31,19 +31,22 @@ pub struct KeyDispatcher { } #[derive(Default)] -pub struct DispatchNode { - key_listeners: SmallVec<[KeyListener; 2]>, - action_listeners: SmallVec<[ActionListener; 16]>, - context: KeyContext, +pub(crate) struct DispatchNode { + pub key_listeners: SmallVec<[KeyListener; 2]>, + pub action_listeners: SmallVec<[ActionListener; 16]>, + pub context: KeyContext, parent: Option, } -struct ActionListener { - action_type: TypeId, - listener: Box, +type KeyListener = Rc; + +#[derive(Clone)] +pub(crate) struct ActionListener { + pub(crate) action_type: TypeId, + pub(crate) listener: Rc, } -impl KeyDispatcher { +impl DispatchTree { pub fn new(keymap: Arc>) -> Self { Self { node_stack: Vec::new(), @@ -97,7 +100,7 @@ impl KeyDispatcher { pub fn on_action( &mut self, action_type: TypeId, - listener: Box, + listener: Rc, ) { self.active_node().action_listeners.push(ActionListener { action_type, @@ -140,143 +143,40 @@ impl KeyDispatcher { actions } - pub fn dispatch_key(&mut self, target: FocusId, event: &dyn Any, cx: &mut WindowContext) { - if let Some(target_node_id) = self.focusable_node_ids.get(&target).copied() { - self.dispatch_key_on_node(target_node_id, event, cx); - } - } - - fn dispatch_key_on_node( + pub fn dispatch_key( &mut self, - node_id: DispatchNodeId, - event: &dyn Any, - cx: &mut WindowContext, - ) { - let dispatch_path = self.dispatch_path(node_id); - - // Capture phase - self.context_stack.clear(); - cx.propagate_event = true; - - for node_id in &dispatch_path { - let node = &self.nodes[node_id.0]; - if !node.context.is_empty() { - self.context_stack.push(node.context.clone()); - } - - for key_listener in &node.key_listeners { - key_listener(event, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; - } - } + keystroke: &Keystroke, + context: &[KeyContext], + ) -> Option> { + if !self + .keystroke_matchers + .contains_key(self.context_stack.as_slice()) + { + let keystroke_contexts = self.context_stack.iter().cloned().collect(); + self.keystroke_matchers.insert( + keystroke_contexts, + KeystrokeMatcher::new(self.keymap.clone()), + ); } - // Bubble phase - for node_id in dispatch_path.iter().rev() { - let node = &self.nodes[node_id.0]; - - // Handle low level key events - for key_listener in &node.key_listeners { - key_listener(event, DispatchPhase::Bubble, cx); - if !cx.propagate_event { - return; - } + let keystroke_matcher = self + .keystroke_matchers + .get_mut(self.context_stack.as_slice()) + .unwrap(); + if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke(keystroke, context) { + // Clear all pending keystrokes when an action has been found. + for keystroke_matcher in self.keystroke_matchers.values_mut() { + keystroke_matcher.clear_pending(); } - // Match keystrokes - if !node.context.is_empty() { - if let Some(key_down_event) = event.downcast_ref::() { - if !self - .keystroke_matchers - .contains_key(self.context_stack.as_slice()) - { - let keystroke_contexts = self.context_stack.iter().cloned().collect(); - self.keystroke_matchers.insert( - keystroke_contexts, - KeystrokeMatcher::new(self.keymap.clone()), - ); - } - - let keystroke_matcher = self - .keystroke_matchers - .get_mut(self.context_stack.as_slice()) - .unwrap(); - if let KeyMatch::Some(action) = keystroke_matcher - .match_keystroke(&key_down_event.keystroke, self.context_stack.as_slice()) - { - // Clear all pending keystrokes when an action has been found. - for keystroke_matcher in self.keystroke_matchers.values_mut() { - keystroke_matcher.clear_pending(); - } - - self.dispatch_action_on_node(*node_id, action, cx); - if !cx.propagate_event { - return; - } - } - } - - self.context_stack.pop(); - } + Some(action) + } else { + None } } - pub fn dispatch_action( - &self, - target: FocusId, - action: Box, - cx: &mut WindowContext, - ) { - if let Some(target_node_id) = self.focusable_node_ids.get(&target).copied() { - self.dispatch_action_on_node(target_node_id, action, cx); - } - } - - fn dispatch_action_on_node( - &self, - node_id: DispatchNodeId, - action: Box, - cx: &mut WindowContext, - ) { - let dispatch_path = self.dispatch_path(node_id); - - // Capture phase - for node_id in &dispatch_path { - let node = &self.nodes[node_id.0]; - for ActionListener { - action_type, - listener, - } in &node.action_listeners - { - let any_action = action.as_any(); - if *action_type == any_action.type_id() { - listener(any_action, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; - } - } - } - } - - // Bubble phase - for node_id in dispatch_path.iter().rev() { - let node = &self.nodes[node_id.0]; - for ActionListener { - action_type, - listener, - } in &node.action_listeners - { - let any_action = action.as_any(); - if *action_type == any_action.type_id() { - cx.propagate_event = false; // Actions stop propagation by default during the bubble phase - listener(any_action, DispatchPhase::Bubble, cx); - if !cx.propagate_event { - return; - } - } - } - } + pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { + &self.nodes[node_id.0] } fn active_node(&mut self) -> &mut DispatchNode { @@ -288,8 +188,7 @@ impl KeyDispatcher { *self.node_stack.last().unwrap() } - /// Returns the DispatchNodeIds from the root of the tree to the given target node id. - fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { + pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new(); let mut current_node_id = Some(target); while let Some(node_id) = current_node_id { @@ -299,6 +198,10 @@ impl KeyDispatcher { dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. dispatch_path } + + pub fn focusable_node_id(&self, target: FocusId) -> Option { + self.focusable_node_ids.get(&target).copied() + } } pub trait KeyDispatch: 'static { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 82d5982475..f574d7eb5f 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,14 +1,15 @@ use crate::{ - px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, - Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DisplayId, Edges, Effect, - Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, - Hsla, ImageData, InputEvent, IsZero, KeyContext, KeyDispatcher, LayoutId, Model, ModelContext, - Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, - Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, - PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, - RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, - Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, - WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + key_dispatch::ActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, + AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, + DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, + EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, + InputEvent, IsZero, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, Modifiers, + MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, + PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, + PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, + SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, + TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, + WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -89,9 +90,7 @@ impl FocusId { pub(crate) fn contains(&self, other: Self, cx: &WindowContext) -> bool { cx.window .current_frame - .key_dispatcher - .as_ref() - .unwrap() + .dispatch_tree .focus_contains(*self, other) } } @@ -213,7 +212,7 @@ pub struct Window { pub(crate) struct Frame { element_states: HashMap, mouse_listeners: HashMap>, - pub(crate) key_dispatcher: Option, + pub(crate) dispatch_tree: DispatchTree, pub(crate) focus_listeners: Vec, pub(crate) scene_builder: SceneBuilder, z_index_stack: StackingOrder, @@ -222,11 +221,11 @@ pub(crate) struct Frame { } impl Frame { - pub fn new(key_dispatcher: KeyDispatcher) -> Self { + pub fn new(dispatch_tree: DispatchTree) -> Self { Frame { element_states: HashMap::default(), mouse_listeners: HashMap::default(), - key_dispatcher: Some(key_dispatcher), + dispatch_tree, focus_listeners: Vec::new(), scene_builder: SceneBuilder::default(), z_index_stack: StackingOrder::default(), @@ -302,8 +301,8 @@ impl Window { layout_engine: TaffyLayoutEngine::new(), root_view: None, element_id_stack: GlobalElementId::default(), - previous_frame: Frame::new(KeyDispatcher::new(cx.keymap.clone())), - current_frame: Frame::new(KeyDispatcher::new(cx.keymap.clone())), + previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())), + current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), focus_listeners: SubscriberSet::new(), default_prevented: true, @@ -423,9 +422,14 @@ impl<'a> WindowContext<'a> { pub fn dispatch_action(&mut self, action: Box) { if let Some(focus_handle) = self.focused() { self.defer(move |cx| { - let dispatcher = cx.window.current_frame.key_dispatcher.take().unwrap(); - dispatcher.dispatch_action(focus_handle.id, action, cx); - cx.window.current_frame.key_dispatcher = Some(dispatcher); + if let Some(node_id) = cx + .window + .current_frame + .dispatch_tree + .focusable_node_id(focus_handle.id) + { + cx.dispatch_action_on_node(node_id, action); + } }) } } @@ -723,12 +727,14 @@ impl<'a> WindowContext<'a> { &mut self, handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, ) { - let key_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); - key_dispatcher.on_key_event(Box::new(move |event, phase, cx| { - if let Some(event) = event.downcast_ref::() { - handler(event, phase, cx) - } - })); + self.window + .current_frame + .dispatch_tree + .on_key_event(Rc::new(move |event, phase, cx| { + if let Some(event) = event.downcast_ref::() { + handler(event, phase, cx) + } + })); } /// Register an action listener on the window for the current frame. The type of action @@ -742,10 +748,9 @@ impl<'a> WindowContext<'a> { action_type: TypeId, handler: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static, ) { - let key_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); - key_dispatcher.on_action( + self.window.current_frame.dispatch_tree.on_action( action_type, - Box::new(move |action, phase, cx| handler(action, phase, cx)), + Rc::new(move |action, phase, cx| handler(action, phase, cx)), ); } @@ -1110,7 +1115,7 @@ impl<'a> WindowContext<'a> { frame.element_states.clear(); frame.mouse_listeners.values_mut().for_each(Vec::clear); frame.focus_listeners.clear(); - frame.key_dispatcher.as_mut().map(KeyDispatcher::clear); + frame.dispatch_tree.clear(); } /// Dispatch a mouse or keyboard event on the window. @@ -1172,63 +1177,172 @@ impl<'a> WindowContext<'a> { }; if let Some(any_mouse_event) = event.mouse_event() { - if let Some(mut handlers) = self - .window - .current_frame - .mouse_listeners - .remove(&any_mouse_event.type_id()) - { - // Because handlers may add other handlers, we sort every time. - handlers.sort_by(|(a, _), (b, _)| a.cmp(b)); + self.dispatch_mouse_event(any_mouse_event); + } else if let Some(any_key_event) = event.keyboard_event() { + self.dispatch_key_event(any_key_event); + } - // Capture phase, events bubble from back to front. Handlers for this phase are used for - // special purposes, such as detecting events outside of a given Bounds. - for (_, handler) in &mut handlers { - handler(any_mouse_event, DispatchPhase::Capture, self); + !self.app.propagate_event + } + + fn dispatch_mouse_event(&mut self, event: &dyn Any) { + if let Some(mut handlers) = self + .window + .current_frame + .mouse_listeners + .remove(&event.type_id()) + { + // Because handlers may add other handlers, we sort every time. + handlers.sort_by(|(a, _), (b, _)| a.cmp(b)); + + // Capture phase, events bubble from back to front. Handlers for this phase are used for + // special purposes, such as detecting events outside of a given Bounds. + for (_, handler) in &mut handlers { + handler(event, DispatchPhase::Capture, self); + if !self.app.propagate_event { + break; + } + } + + // Bubble phase, where most normal handlers do their work. + if self.app.propagate_event { + for (_, handler) in handlers.iter_mut().rev() { + handler(event, DispatchPhase::Bubble, self); if !self.app.propagate_event { break; } } + } - // Bubble phase, where most normal handlers do their work. - if self.app.propagate_event { - for (_, handler) in handlers.iter_mut().rev() { - handler(any_mouse_event, DispatchPhase::Bubble, self); - if !self.app.propagate_event { - break; - } - } - } + if self.app.propagate_event && event.downcast_ref::().is_some() { + self.active_drag = None; + } - if self.app.propagate_event - && any_mouse_event.downcast_ref::().is_some() - { - self.active_drag = None; - } - - // Just in case any handlers added new handlers, which is weird, but possible. - handlers.extend( - self.window - .current_frame - .mouse_listeners - .get_mut(&any_mouse_event.type_id()) - .into_iter() - .flat_map(|handlers| handlers.drain(..)), - ); + // Just in case any handlers added new handlers, which is weird, but possible. + handlers.extend( self.window .current_frame .mouse_listeners - .insert(any_mouse_event.type_id(), handlers); + .get_mut(&event.type_id()) + .into_iter() + .flat_map(|handlers| handlers.drain(..)), + ); + self.window + .current_frame + .mouse_listeners + .insert(event.type_id(), handlers); + } + } + + fn dispatch_key_event(&mut self, event: &dyn Any) { + if let Some(node_id) = self.window.focus.and_then(|focus_id| { + self.window + .current_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) { + let dispatch_path = self + .window + .current_frame + .dispatch_tree + .dispatch_path(node_id); + + // Capture phase + let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new(); + self.propagate_event = true; + + for node_id in &dispatch_path { + let node = self.window.current_frame.dispatch_tree.node(*node_id); + + if !node.context.is_empty() { + context_stack.push(node.context.clone()); + } + + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Capture, self); + if !self.propagate_event { + return; + } + } } - } else if let Some(any_key_event) = event.keyboard_event() { - if let Some(focus_id) = self.window.focus { - let mut dispatcher = self.window.current_frame.key_dispatcher.take().unwrap(); - dispatcher.dispatch_key(focus_id, any_key_event, self); - self.window.current_frame.key_dispatcher = Some(dispatcher); + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + // Handle low level key events + let node = self.window.current_frame.dispatch_tree.node(*node_id); + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Bubble, self); + if !self.propagate_event { + return; + } + } + + // Match keystrokes + let node = self.window.current_frame.dispatch_tree.node(*node_id); + if !node.context.is_empty() { + if let Some(key_down_event) = event.downcast_ref::() { + if let Some(action) = self + .window + .current_frame + .dispatch_tree + .dispatch_key(&key_down_event.keystroke, &context_stack) + { + self.dispatch_action_on_node(*node_id, action); + if !self.propagate_event { + return; + } + } + } + + context_stack.pop(); + } + } + } + } + + fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box) { + let dispatch_path = self + .window + .current_frame + .dispatch_tree + .dispatch_path(node_id); + + // Capture phase + for node_id in &dispatch_path { + let node = self.window.current_frame.dispatch_tree.node(*node_id); + for ActionListener { + action_type, + listener, + } in node.action_listeners.clone() + { + let any_action = action.as_any(); + if action_type == any_action.type_id() { + listener(any_action, DispatchPhase::Capture, self); + if !self.propagate_event { + return; + } + } } } - !self.app.propagate_event + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = self.window.current_frame.dispatch_tree.node(*node_id); + for ActionListener { + action_type, + listener, + } in node.action_listeners.clone() + { + let any_action = action.as_any(); + if action_type == any_action.type_id() { + self.propagate_event = false; // Actions stop propagation by default during the bubble phase + listener(any_action, DispatchPhase::Bubble, self); + if !self.propagate_event { + return; + } + } + } + } } /// Register the given handler to be invoked whenever the global of the given type @@ -1261,9 +1375,7 @@ impl<'a> WindowContext<'a> { if let Some(focus_id) = self.window.focus { self.window .current_frame - .key_dispatcher - .as_ref() - .unwrap() + .dispatch_tree .available_actions(focus_id) } else { Vec::new() @@ -1926,17 +2038,20 @@ impl<'a, V: 'static> ViewContext<'a, V> { f: impl FnOnce(Option, &mut Self) -> R, ) -> R { let window = &mut self.window; - let old_dispatcher = window.previous_frame.key_dispatcher.as_mut().unwrap(); - let current_dispatcher = window.current_frame.key_dispatcher.as_mut().unwrap(); - current_dispatcher.push_node(context, old_dispatcher); + window + .current_frame + .dispatch_tree + .push_node(context, &mut window.previous_frame.dispatch_tree); if let Some(focus_handle) = focus_handle.as_ref() { - current_dispatcher.make_focusable(focus_handle.id); + window + .current_frame + .dispatch_tree + .make_focusable(focus_handle.id); } let result = f(focus_handle, self); - let current_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); - current_dispatcher.pop_node(); + self.window.current_frame.dispatch_tree.pop_node(); result } From 348760556aded99003930b03da362e69b9f76347 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 18:33:08 +0100 Subject: [PATCH 028/126] :lipstick: --- crates/gpui2/src/key_dispatch.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index bc8e1f8f85..e517b8d34b 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -175,19 +175,6 @@ impl DispatchTree { } } - pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { - &self.nodes[node_id.0] - } - - fn active_node(&mut self) -> &mut DispatchNode { - let active_node_id = self.active_node_id(); - &mut self.nodes[active_node_id.0] - } - - fn active_node_id(&self) -> DispatchNodeId { - *self.node_stack.last().unwrap() - } - pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new(); let mut current_node_id = Some(target); @@ -199,9 +186,22 @@ impl DispatchTree { dispatch_path } + pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { + &self.nodes[node_id.0] + } + + fn active_node(&mut self) -> &mut DispatchNode { + let active_node_id = self.active_node_id(); + &mut self.nodes[active_node_id.0] + } + pub fn focusable_node_id(&self, target: FocusId) -> Option { self.focusable_node_ids.get(&target).copied() } + + fn active_node_id(&self) -> DispatchNodeId { + *self.node_stack.last().unwrap() + } } pub trait KeyDispatch: 'static { From 7be12cb7b1b9dec84dfed46699eebbce3430ad48 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 12:44:50 -0500 Subject: [PATCH 029/126] Checkpoint Broken --- crates/theme2/src/default_colors.rs | 70 +++++++++++------------ crates/theme2/src/styles.rs | 10 +++- crates/theme2/src/{ => styles}/colors.rs | 9 +-- crates/theme2/src/{ => styles}/players.rs | 0 crates/theme2/src/styles/status.rs | 46 ++++++++++++++- crates/theme2/src/{ => styles}/syntax.rs | 0 crates/theme2/src/styles/system.rs | 9 +++ 7 files changed, 99 insertions(+), 45 deletions(-) rename crates/theme2/src/{ => styles}/colors.rs (98%) rename crates/theme2/src/{ => styles}/players.rs (100%) rename crates/theme2/src/{ => styles}/syntax.rs (100%) create mode 100644 crates/theme2/src/styles/system.rs diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 43d6259e52..2fd2ec7a8a 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -1,6 +1,6 @@ use gpui::{hsla, Hsla, Rgba}; -use crate::colors::{StatusColors, SystemColors, ThemeColors}; +use crate::colors::{SystemColors, ThemeColors}; use crate::scale::{ColorScaleSet, ColorScales}; use crate::syntax::SyntaxTheme; use crate::{ColorScale, PlayerColor, PlayerColors}; @@ -99,7 +99,7 @@ impl PlayerColors { } } -fn neutral() -> ColorScaleSet { +pub(crate) fn neutral() -> ColorScaleSet { slate() } @@ -452,7 +452,7 @@ pub fn default_color_scales() -> ColorScales { } } -fn gray() -> ColorScaleSet { +pub(crate) fn gray() -> ColorScaleSet { StaticColorScaleSet { scale: "Gray", light: [ @@ -516,7 +516,7 @@ fn gray() -> ColorScaleSet { .unwrap() } -fn mauve() -> ColorScaleSet { +pub(crate) fn mauve() -> ColorScaleSet { StaticColorScaleSet { scale: "Mauve", light: [ @@ -580,7 +580,7 @@ fn mauve() -> ColorScaleSet { .unwrap() } -fn slate() -> ColorScaleSet { +pub(crate) fn slate() -> ColorScaleSet { StaticColorScaleSet { scale: "Slate", light: [ @@ -644,7 +644,7 @@ fn slate() -> ColorScaleSet { .unwrap() } -fn sage() -> ColorScaleSet { +pub(crate) fn sage() -> ColorScaleSet { StaticColorScaleSet { scale: "Sage", light: [ @@ -708,7 +708,7 @@ fn sage() -> ColorScaleSet { .unwrap() } -fn olive() -> ColorScaleSet { +pub(crate) fn olive() -> ColorScaleSet { StaticColorScaleSet { scale: "Olive", light: [ @@ -772,7 +772,7 @@ fn olive() -> ColorScaleSet { .unwrap() } -fn sand() -> ColorScaleSet { +pub(crate) fn sand() -> ColorScaleSet { StaticColorScaleSet { scale: "Sand", light: [ @@ -836,7 +836,7 @@ fn sand() -> ColorScaleSet { .unwrap() } -fn gold() -> ColorScaleSet { +pub(crate) fn gold() -> ColorScaleSet { StaticColorScaleSet { scale: "Gold", light: [ @@ -900,7 +900,7 @@ fn gold() -> ColorScaleSet { .unwrap() } -fn bronze() -> ColorScaleSet { +pub(crate) fn bronze() -> ColorScaleSet { StaticColorScaleSet { scale: "Bronze", light: [ @@ -964,7 +964,7 @@ fn bronze() -> ColorScaleSet { .unwrap() } -fn brown() -> ColorScaleSet { +pub(crate) fn brown() -> ColorScaleSet { StaticColorScaleSet { scale: "Brown", light: [ @@ -1028,7 +1028,7 @@ fn brown() -> ColorScaleSet { .unwrap() } -fn yellow() -> ColorScaleSet { +pub(crate) fn yellow() -> ColorScaleSet { StaticColorScaleSet { scale: "Yellow", light: [ @@ -1092,7 +1092,7 @@ fn yellow() -> ColorScaleSet { .unwrap() } -fn amber() -> ColorScaleSet { +pub(crate) fn amber() -> ColorScaleSet { StaticColorScaleSet { scale: "Amber", light: [ @@ -1156,7 +1156,7 @@ fn amber() -> ColorScaleSet { .unwrap() } -fn orange() -> ColorScaleSet { +pub(crate) fn orange() -> ColorScaleSet { StaticColorScaleSet { scale: "Orange", light: [ @@ -1220,7 +1220,7 @@ fn orange() -> ColorScaleSet { .unwrap() } -fn tomato() -> ColorScaleSet { +pub(crate) fn tomato() -> ColorScaleSet { StaticColorScaleSet { scale: "Tomato", light: [ @@ -1284,7 +1284,7 @@ fn tomato() -> ColorScaleSet { .unwrap() } -fn red() -> ColorScaleSet { +pub(crate) fn red() -> ColorScaleSet { StaticColorScaleSet { scale: "Red", light: [ @@ -1348,7 +1348,7 @@ fn red() -> ColorScaleSet { .unwrap() } -fn ruby() -> ColorScaleSet { +pub(crate) fn ruby() -> ColorScaleSet { StaticColorScaleSet { scale: "Ruby", light: [ @@ -1412,7 +1412,7 @@ fn ruby() -> ColorScaleSet { .unwrap() } -fn crimson() -> ColorScaleSet { +pub(crate) fn crimson() -> ColorScaleSet { StaticColorScaleSet { scale: "Crimson", light: [ @@ -1476,7 +1476,7 @@ fn crimson() -> ColorScaleSet { .unwrap() } -fn pink() -> ColorScaleSet { +pub(crate) fn pink() -> ColorScaleSet { StaticColorScaleSet { scale: "Pink", light: [ @@ -1540,7 +1540,7 @@ fn pink() -> ColorScaleSet { .unwrap() } -fn plum() -> ColorScaleSet { +pub(crate) fn plum() -> ColorScaleSet { StaticColorScaleSet { scale: "Plum", light: [ @@ -1604,7 +1604,7 @@ fn plum() -> ColorScaleSet { .unwrap() } -fn purple() -> ColorScaleSet { +pub(crate) fn purple() -> ColorScaleSet { StaticColorScaleSet { scale: "Purple", light: [ @@ -1668,7 +1668,7 @@ fn purple() -> ColorScaleSet { .unwrap() } -fn violet() -> ColorScaleSet { +pub(crate) fn violet() -> ColorScaleSet { StaticColorScaleSet { scale: "Violet", light: [ @@ -1732,7 +1732,7 @@ fn violet() -> ColorScaleSet { .unwrap() } -fn iris() -> ColorScaleSet { +pub(crate) fn iris() -> ColorScaleSet { StaticColorScaleSet { scale: "Iris", light: [ @@ -1796,7 +1796,7 @@ fn iris() -> ColorScaleSet { .unwrap() } -fn indigo() -> ColorScaleSet { +pub(crate) fn indigo() -> ColorScaleSet { StaticColorScaleSet { scale: "Indigo", light: [ @@ -1860,7 +1860,7 @@ fn indigo() -> ColorScaleSet { .unwrap() } -fn blue() -> ColorScaleSet { +pub(crate) fn blue() -> ColorScaleSet { StaticColorScaleSet { scale: "Blue", light: [ @@ -1924,7 +1924,7 @@ fn blue() -> ColorScaleSet { .unwrap() } -fn cyan() -> ColorScaleSet { +pub(crate) fn cyan() -> ColorScaleSet { StaticColorScaleSet { scale: "Cyan", light: [ @@ -1988,7 +1988,7 @@ fn cyan() -> ColorScaleSet { .unwrap() } -fn teal() -> ColorScaleSet { +pub(crate) fn teal() -> ColorScaleSet { StaticColorScaleSet { scale: "Teal", light: [ @@ -2052,7 +2052,7 @@ fn teal() -> ColorScaleSet { .unwrap() } -fn jade() -> ColorScaleSet { +pub(crate) fn jade() -> ColorScaleSet { StaticColorScaleSet { scale: "Jade", light: [ @@ -2116,7 +2116,7 @@ fn jade() -> ColorScaleSet { .unwrap() } -fn green() -> ColorScaleSet { +pub(crate) fn green() -> ColorScaleSet { StaticColorScaleSet { scale: "Green", light: [ @@ -2180,7 +2180,7 @@ fn green() -> ColorScaleSet { .unwrap() } -fn grass() -> ColorScaleSet { +pub(crate) fn grass() -> ColorScaleSet { StaticColorScaleSet { scale: "Grass", light: [ @@ -2244,7 +2244,7 @@ fn grass() -> ColorScaleSet { .unwrap() } -fn lime() -> ColorScaleSet { +pub(crate) fn lime() -> ColorScaleSet { StaticColorScaleSet { scale: "Lime", light: [ @@ -2308,7 +2308,7 @@ fn lime() -> ColorScaleSet { .unwrap() } -fn mint() -> ColorScaleSet { +pub(crate) fn mint() -> ColorScaleSet { StaticColorScaleSet { scale: "Mint", light: [ @@ -2372,7 +2372,7 @@ fn mint() -> ColorScaleSet { .unwrap() } -fn sky() -> ColorScaleSet { +pub(crate) fn sky() -> ColorScaleSet { StaticColorScaleSet { scale: "Sky", light: [ @@ -2436,7 +2436,7 @@ fn sky() -> ColorScaleSet { .unwrap() } -fn black() -> ColorScaleSet { +pub(crate) fn black() -> ColorScaleSet { StaticColorScaleSet { scale: "Black", light: [ @@ -2500,7 +2500,7 @@ fn black() -> ColorScaleSet { .unwrap() } -fn white() -> ColorScaleSet { +pub(crate) fn white() -> ColorScaleSet { StaticColorScaleSet { scale: "White", light: [ diff --git a/crates/theme2/src/styles.rs b/crates/theme2/src/styles.rs index 0a44e2a468..18f9e76581 100644 --- a/crates/theme2/src/styles.rs +++ b/crates/theme2/src/styles.rs @@ -1,3 +1,11 @@ +mod colors; +mod players; mod status; +mod syntax; +mod system; -use status::*; +pub use colors::*; +pub use players::*; +pub use status::*; +pub use syntax::*; +pub use system::*; diff --git a/crates/theme2/src/colors.rs b/crates/theme2/src/styles/colors.rs similarity index 98% rename from crates/theme2/src/colors.rs rename to crates/theme2/src/styles/colors.rs index aab672ad57..3104f46705 100644 --- a/crates/theme2/src/colors.rs +++ b/crates/theme2/src/styles/colors.rs @@ -1,15 +1,8 @@ -use crate::{PlayerColors, SyntaxTheme}; use gpui::Hsla; use refineable::Refineable; use std::sync::Arc; -#[derive(Clone)] -pub struct SystemColors { - pub transparent: Hsla, - pub mac_os_traffic_light_red: Hsla, - pub mac_os_traffic_light_yellow: Hsla, - pub mac_os_traffic_light_green: Hsla, -} +use crate::{PlayerColors, SyntaxTheme, SystemColors}; #[derive(Refineable, Clone, Debug)] #[refineable(Debug, serde::Deserialize)] diff --git a/crates/theme2/src/players.rs b/crates/theme2/src/styles/players.rs similarity index 100% rename from crates/theme2/src/players.rs rename to crates/theme2/src/styles/players.rs diff --git a/crates/theme2/src/styles/status.rs b/crates/theme2/src/styles/status.rs index 87f9da96ee..cf2a1b64f6 100644 --- a/crates/theme2/src/styles/status.rs +++ b/crates/theme2/src/styles/status.rs @@ -1,4 +1,29 @@ -use crate::StatusColors; +use gpui::Hsla; + +use crate::{blue, grass, neutral, red, yellow, StatusColors}; + +impl Default for StatusColors { + /// Don't use this! + /// We have to have a default for StatusColors to be `[refineable::Refinable]`. + fn default() -> Self { + Self::dark() + } +} + +pub struct DiagnosticColors { + pub error: Hsla, + pub warning: Hsla, + pub info: Hsla, +} + +pub struct GitStatusColors { + pub created: Hsla, + pub deleted: Hsla, + pub modified: Hsla, + pub renamed: Hsla, + pub conflict: Hsla, + pub ignored: Hsla, +} impl StatusColors { pub fn dark() -> Self { @@ -38,4 +63,23 @@ impl StatusColors { warning: yellow().light().step_9(), } } + + pub fn diagnostic(&self) -> DiagnosticColors { + DiagnosticColors { + error: self.error, + warning: self.warning, + info: self.info, + } + } + + pub fn git(&self) -> GitStatusColors { + GitStatusColors { + created: self.created, + deleted: self.deleted, + modified: self.modified, + renamed: self.renamed, + conflict: self.conflict, + ignored: self.ignored, + } + } } diff --git a/crates/theme2/src/syntax.rs b/crates/theme2/src/styles/syntax.rs similarity index 100% rename from crates/theme2/src/syntax.rs rename to crates/theme2/src/styles/syntax.rs diff --git a/crates/theme2/src/styles/system.rs b/crates/theme2/src/styles/system.rs new file mode 100644 index 0000000000..6a7ca101e2 --- /dev/null +++ b/crates/theme2/src/styles/system.rs @@ -0,0 +1,9 @@ +use gpui::Hsla; + +#[derive(Clone)] +pub struct SystemColors { + pub transparent: Hsla, + pub mac_os_traffic_light_red: Hsla, + pub mac_os_traffic_light_yellow: Hsla, + pub mac_os_traffic_light_green: Hsla, +} From f6c54b804397f2c01625c7ffbdf729042bc23412 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 13:13:40 -0500 Subject: [PATCH 030/126] Redine command palette style Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> Co-Authored-By: Conrad Irwin --- .../command_palette2/src/command_palette.rs | 24 +++++--- crates/picker2/src/picker2.rs | 58 ++++++++++++------- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index bf9f9fa94b..508707f264 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -2,13 +2,13 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke, - ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext, - WeakView, WindowContext, + ParentElement, Render, SharedString, StatelessInteractive, Styled, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; use std::cmp::{self, Reverse}; use theme::ActiveTheme; -use ui::{v_stack, Label, StyledExt}; +use ui::{v_stack, HighlightedLabel, StyledExt}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -147,6 +147,10 @@ impl CommandPaletteDelegate { impl PickerDelegate for CommandPaletteDelegate { type ListItem = Div>; + fn placeholder_text(&self) -> Arc { + "Execute a command...".into() + } + fn match_count(&self) -> usize { self.matches.len() } @@ -296,11 +300,10 @@ impl PickerDelegate for CommandPaletteDelegate { cx: &mut ViewContext>, ) -> Self::ListItem { let colors = cx.theme().colors(); - let Some(command) = self - .matches - .get(ix) - .and_then(|m| self.commands.get(m.candidate_id)) - else { + let Some(r#match) = self.matches.get(ix) else { + return div(); + }; + let Some(command) = self.commands.get(r#match.candidate_id) else { return div(); }; @@ -312,7 +315,10 @@ impl PickerDelegate for CommandPaletteDelegate { .rounded_md() .when(selected, |this| this.bg(colors.ghost_element_selected)) .hover(|this| this.bg(colors.ghost_element_hover)) - .child(Label::new(command.name.clone())) + .child(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) } // fn render_match( diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index ac1c5f89ea..1c42e2ed3f 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -5,7 +5,7 @@ use gpui::{ WindowContext, }; use std::cmp; -use ui::{prelude::*, v_stack, Divider}; +use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; pub struct Picker { pub delegate: D, @@ -21,7 +21,7 @@ pub trait PickerDelegate: Sized + 'static { fn selected_index(&self) -> usize; fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); - // fn placeholder_text(&self) -> Arc; + fn placeholder_text(&self) -> Arc; fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); @@ -37,7 +37,11 @@ pub trait PickerDelegate: Sized + 'static { impl Picker { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { - let editor = cx.build_view(|cx| Editor::single_line(cx)); + let editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text(delegate.placeholder_text(), cx); + editor + }); cx.subscribe(&editor, Self::on_input_editor_event).detach(); Self { delegate, @@ -159,23 +163,35 @@ impl Render for Picker { .child(div().px_1().py_0p5().child(self.editor.clone())), ) .child(Divider::horizontal()) - .child( - v_stack() - .p_1() - .grow() - .child( - uniform_list("candidates", self.delegate.match_count(), { - move |this: &mut Self, visible_range, cx| { - let selected_ix = this.delegate.selected_index(); - visible_range - .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx)) - .collect() - } - }) - .track_scroll(self.scroll_handle.clone()), - ) - .max_h_72() - .overflow_hidden(), - ) + .when(self.delegate.match_count() > 0, |el| { + el.child( + v_stack() + .p_1() + .grow() + .child( + uniform_list("candidates", self.delegate.match_count(), { + move |this: &mut Self, visible_range, cx| { + let selected_ix = this.delegate.selected_index(); + visible_range + .map(|ix| { + this.delegate.render_match(ix, ix == selected_ix, cx) + }) + .collect() + } + }) + .track_scroll(self.scroll_handle.clone()), + ) + .max_h_72() + .overflow_hidden(), + ) + }) + .when(self.delegate.match_count() == 0, |el| { + el.child( + v_stack() + .p_1() + .grow() + .child(Label::new("No matches").color(LabelColor::Muted)), + ) + }) } } From 8432b713cc53984c2ecf09cae7de038abd2d5443 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 13:16:05 -0500 Subject: [PATCH 031/126] Resolve errors Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/command_palette2/src/command_palette.rs | 9 ++++++--- crates/picker2/src/picker2.rs | 2 +- crates/storybook2/src/stories/picker.rs | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 508707f264..c7a6c9ee83 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -2,11 +2,14 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke, - ParentElement, Render, SharedString, StatelessInteractive, Styled, View, ViewContext, - VisualContext, WeakView, WindowContext, + ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext, + WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; -use std::cmp::{self, Reverse}; +use std::{ + cmp::{self, Reverse}, + sync::Arc, +}; use theme::ActiveTheme; use ui::{v_stack, HighlightedLabel, StyledExt}; use util::{ diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 1c42e2ed3f..0a731b4a27 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -4,7 +4,7 @@ use gpui::{ StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; -use std::cmp; +use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; pub struct Picker { diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 82a010e6b3..067c190575 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -44,6 +44,10 @@ impl PickerDelegate for Delegate { self.candidates.len() } + fn placeholder_text(&self) -> Arc { + "Test".into() + } + fn render_match( &self, ix: usize, From 2625051f75adf52c046a1ffd404ef3d2c669c373 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 11:32:05 -0700 Subject: [PATCH 032/126] Better fix for multiple focuses in one frame --- crates/gpui2/src/app.rs | 17 +++++++++++------ crates/gpui2/src/window.rs | 4 ---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 356cf1b76b..61c6195d90 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -641,14 +641,19 @@ impl AppContext { // The window might change focus multiple times in an effect cycle. // We only honor effects for the most recently focused handle. if cx.window.focus == focused { + // if someone calls focus multiple times in one frame with the same handle + // the first apply_focus_changed_effect will have taken the last blur already + // and run the rest of this, so we can return. + let Some(last_blur) = cx.window.last_blur.take() else { + return; + }; + let focused = focused .map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap()); - let blurred = cx - .window - .last_blur - .take() - .unwrap() - .and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles)); + + let blurred = + last_blur.and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles)); + let focus_changed = focused.is_some() || blurred.is_some(); let event = FocusEvent { focused, blurred }; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index f574d7eb5f..11878c15fa 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -389,10 +389,6 @@ impl<'a> WindowContext<'a> { pub fn focus(&mut self, handle: &FocusHandle) { let focus_id = handle.id; - if self.window.focus == Some(focus_id) { - return; - } - if self.window.last_blur.is_none() { self.window.last_blur = Some(self.window.focus); } From 13dd9128170b70e95d0d163f06b6bcd8a9013425 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 13 Nov 2023 10:47:15 -0800 Subject: [PATCH 033/126] Get left, right, and bottom docks rendering in the right places in the workspace Co-authored-by: Julia Co-authored-by: Marshall --- crates/gpui2/src/color.rs | 18 +++++++ crates/project_panel2/src/project_panel.rs | 6 ++- crates/workspace2/src/dock.rs | 8 ++- crates/workspace2/src/workspace2.rs | 61 ++++++++-------------- 4 files changed, 51 insertions(+), 42 deletions(-) diff --git a/crates/gpui2/src/color.rs b/crates/gpui2/src/color.rs index 6fcb12e178..d8989878de 100644 --- a/crates/gpui2/src/color.rs +++ b/crates/gpui2/src/color.rs @@ -238,6 +238,24 @@ pub fn blue() -> Hsla { } } +pub fn green() -> Hsla { + Hsla { + h: 0.33, + s: 1., + l: 0.5, + a: 1., + } +} + +pub fn yellow() -> Hsla { + Hsla { + h: 0.16, + s: 1., + l: 0.5, + a: 1., + } +} + impl Hsla { /// Returns true if the HSLA color is fully transparent, false otherwise. pub fn is_transparent(&self) -> bool { diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index e3e04f5254..fec44e3d68 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -31,7 +31,7 @@ use std::{ sync::Arc, }; use theme::ActiveTheme as _; -use ui::{h_stack, v_stack}; +use ui::{h_stack, v_stack, Label}; use unicase::UniCase; use util::TryFutureExt; use workspace::{ @@ -1382,7 +1382,7 @@ impl ProjectPanel { if let (Some(editor), true) = (editor, show_editor) { div().child(editor.clone()) } else { - div().child(details.filename.clone()) + div().child(Label::new(details.filename.clone())) } .ml_1(), ) @@ -1449,6 +1449,7 @@ impl Render for ProjectPanel { if has_worktree { div() + .size_full() .id("project-panel") .track_focus(&self.focus_handle) .child( @@ -1472,6 +1473,7 @@ impl Render for ProjectPanel { items }, ) + .size_full() .track_scroll(self.list.clone()), ) } else { diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index c8ff58f42d..9a614bc92e 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,7 +1,7 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, FocusHandle, - ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext, + ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -428,7 +428,11 @@ impl Render for Dock { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - todo!() + if let Some(entry) = self.visible_entry() { + div().size_full().child(entry.panel.to_any()) + } else { + div() + } } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index c9fd322ed3..8c1c5d9a02 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3642,45 +3642,30 @@ impl Render for Workspace { .border_t() .border_b() .border_color(cx.theme().colors().border) - // .children( - // Some( - // Panel::new("project-panel-outer", cx) - // .side(PanelSide::Left) - // .child(ProjectPanel::new("project-panel-inner")), - // ) - // .filter(|_| self.is_project_panel_open()), - // ) - // .children( - // Some( - // Panel::new("collab-panel-outer", cx) - // .child(CollabPanel::new("collab-panel-inner")) - // .side(PanelSide::Left), - // ) - // .filter(|_| self.is_collab_panel_open()), - // ) - // .child(NotificationToast::new( - // "maxbrunsfeld has requested to add you as a contact.".into(), - // )) .child( - div().flex().flex_col().flex_1().h_full().child( - div().flex().flex_1().child(self.center.render( - &self.project, - &self.follower_states, - self.active_call(), - &self.active_pane, - self.zoomed.as_ref(), - &self.app_state, - cx, - )), - ), // .children( - // Some( - // Panel::new("terminal-panel", cx) - // .child(Terminal::new()) - // .allowed_sides(PanelAllowedSides::BottomOnly) - // .side(PanelSide::Bottom), - // ) - // .filter(|_| self.is_terminal_open()), - // ), + div() + .flex() + .flex_row() + .flex_1() + .h_full() + .child(div().flex().flex_1().child(self.left_dock.clone())) + .child( + div() + .flex() + .flex_col() + .flex_1() + .child(self.center.render( + &self.project, + &self.follower_states, + self.active_call(), + &self.active_pane, + self.zoomed.as_ref(), + &self.app_state, + cx, + )) + .child(div().flex().flex_1().child(self.bottom_dock.clone())), + ) + .child(div().flex().flex_1().child(self.right_dock.clone())), ), // .children( // Some( // Panel::new("chat-panel-outer", cx) From 889d20d0464e735557ecce321bb53d975feb8092 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 13:58:23 -0500 Subject: [PATCH 034/126] Reorganize theme2 crate --- crates/theme2/src/default_colors.rs | 238 +--------------------------- crates/theme2/src/default_theme.rs | 16 +- crates/theme2/src/registry.rs | 8 +- crates/theme2/src/styles/colors.rs | 53 +------ crates/theme2/src/styles/players.rs | 103 ++++++++++++ crates/theme2/src/styles/status.rs | 53 ++++++- crates/theme2/src/styles/syntax.rs | 129 +++++++++++++++ crates/theme2/src/styles/system.rs | 13 +- crates/theme2/src/theme2.rs | 6 - 9 files changed, 315 insertions(+), 304 deletions(-) diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 2fd2ec7a8a..91efecbfb3 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -1,243 +1,15 @@ -use gpui::{hsla, Hsla, Rgba}; +use gpui::{Hsla, Rgba}; -use crate::colors::{SystemColors, ThemeColors}; use crate::scale::{ColorScaleSet, ColorScales}; -use crate::syntax::SyntaxTheme; -use crate::{ColorScale, PlayerColor, PlayerColors}; - -impl Default for PlayerColors { - fn default() -> Self { - Self(vec![ - PlayerColor { - cursor: blue().dark().step_9(), - background: blue().dark().step_5(), - selection: blue().dark().step_3(), - }, - PlayerColor { - cursor: orange().dark().step_9(), - background: orange().dark().step_5(), - selection: orange().dark().step_3(), - }, - PlayerColor { - cursor: pink().dark().step_9(), - background: pink().dark().step_5(), - selection: pink().dark().step_3(), - }, - PlayerColor { - cursor: lime().dark().step_9(), - background: lime().dark().step_5(), - selection: lime().dark().step_3(), - }, - PlayerColor { - cursor: purple().dark().step_9(), - background: purple().dark().step_5(), - selection: purple().dark().step_3(), - }, - PlayerColor { - cursor: amber().dark().step_9(), - background: amber().dark().step_5(), - selection: amber().dark().step_3(), - }, - PlayerColor { - cursor: jade().dark().step_9(), - background: jade().dark().step_5(), - selection: jade().dark().step_3(), - }, - PlayerColor { - cursor: red().dark().step_9(), - background: red().dark().step_5(), - selection: red().dark().step_3(), - }, - ]) - } -} - -impl PlayerColors { - pub fn default_light() -> Self { - Self(vec![ - PlayerColor { - cursor: blue().light().step_9(), - background: blue().light().step_4(), - selection: blue().light().step_3(), - }, - PlayerColor { - cursor: orange().light().step_9(), - background: orange().light().step_4(), - selection: orange().light().step_3(), - }, - PlayerColor { - cursor: pink().light().step_9(), - background: pink().light().step_4(), - selection: pink().light().step_3(), - }, - PlayerColor { - cursor: lime().light().step_9(), - background: lime().light().step_4(), - selection: lime().light().step_3(), - }, - PlayerColor { - cursor: purple().light().step_9(), - background: purple().light().step_4(), - selection: purple().light().step_3(), - }, - PlayerColor { - cursor: amber().light().step_9(), - background: amber().light().step_4(), - selection: amber().light().step_3(), - }, - PlayerColor { - cursor: jade().light().step_9(), - background: jade().light().step_4(), - selection: jade().light().step_3(), - }, - PlayerColor { - cursor: red().light().step_9(), - background: red().light().step_4(), - selection: red().light().step_3(), - }, - ]) - } -} +use crate::ColorScale; +use crate::{SystemColors, ThemeColors}; pub(crate) fn neutral() -> ColorScaleSet { slate() } -impl Default for SystemColors { - fn default() -> Self { - Self { - transparent: hsla(0.0, 0.0, 0.0, 0.0), - mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0), - mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0), - mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0), - } - } -} - -impl SyntaxTheme { - pub fn default_light() -> Self { - Self { - highlights: vec![ - ("attribute".into(), cyan().light().step_11().into()), - ("boolean".into(), tomato().light().step_11().into()), - ("comment".into(), neutral().light().step_11().into()), - ("comment.doc".into(), iris().light().step_12().into()), - ("constant".into(), red().light().step_9().into()), - ("constructor".into(), red().light().step_9().into()), - ("embedded".into(), red().light().step_9().into()), - ("emphasis".into(), red().light().step_9().into()), - ("emphasis.strong".into(), red().light().step_9().into()), - ("enum".into(), red().light().step_9().into()), - ("function".into(), red().light().step_9().into()), - ("hint".into(), red().light().step_9().into()), - ("keyword".into(), orange().light().step_11().into()), - ("label".into(), red().light().step_9().into()), - ("link_text".into(), red().light().step_9().into()), - ("link_uri".into(), red().light().step_9().into()), - ("number".into(), red().light().step_9().into()), - ("operator".into(), red().light().step_9().into()), - ("predictive".into(), red().light().step_9().into()), - ("preproc".into(), red().light().step_9().into()), - ("primary".into(), red().light().step_9().into()), - ("property".into(), red().light().step_9().into()), - ("punctuation".into(), neutral().light().step_11().into()), - ( - "punctuation.bracket".into(), - neutral().light().step_11().into(), - ), - ( - "punctuation.delimiter".into(), - neutral().light().step_11().into(), - ), - ( - "punctuation.list_marker".into(), - blue().light().step_11().into(), - ), - ("punctuation.special".into(), red().light().step_9().into()), - ("string".into(), jade().light().step_11().into()), - ("string.escape".into(), red().light().step_9().into()), - ("string.regex".into(), tomato().light().step_11().into()), - ("string.special".into(), red().light().step_9().into()), - ( - "string.special.symbol".into(), - red().light().step_9().into(), - ), - ("tag".into(), red().light().step_9().into()), - ("text.literal".into(), red().light().step_9().into()), - ("title".into(), red().light().step_9().into()), - ("type".into(), red().light().step_9().into()), - ("variable".into(), red().light().step_9().into()), - ("variable.special".into(), red().light().step_9().into()), - ("variant".into(), red().light().step_9().into()), - ], - inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style") - suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style") - } - } - - pub fn default_dark() -> Self { - Self { - highlights: vec![ - ("attribute".into(), tomato().dark().step_11().into()), - ("boolean".into(), tomato().dark().step_11().into()), - ("comment".into(), neutral().dark().step_11().into()), - ("comment.doc".into(), iris().dark().step_12().into()), - ("constant".into(), orange().dark().step_11().into()), - ("constructor".into(), gold().dark().step_11().into()), - ("embedded".into(), red().dark().step_11().into()), - ("emphasis".into(), red().dark().step_11().into()), - ("emphasis.strong".into(), red().dark().step_11().into()), - ("enum".into(), yellow().dark().step_11().into()), - ("function".into(), blue().dark().step_11().into()), - ("hint".into(), indigo().dark().step_11().into()), - ("keyword".into(), plum().dark().step_11().into()), - ("label".into(), red().dark().step_11().into()), - ("link_text".into(), red().dark().step_11().into()), - ("link_uri".into(), red().dark().step_11().into()), - ("number".into(), red().dark().step_11().into()), - ("operator".into(), red().dark().step_11().into()), - ("predictive".into(), red().dark().step_11().into()), - ("preproc".into(), red().dark().step_11().into()), - ("primary".into(), red().dark().step_11().into()), - ("property".into(), red().dark().step_11().into()), - ("punctuation".into(), neutral().dark().step_11().into()), - ( - "punctuation.bracket".into(), - neutral().dark().step_11().into(), - ), - ( - "punctuation.delimiter".into(), - neutral().dark().step_11().into(), - ), - ( - "punctuation.list_marker".into(), - blue().dark().step_11().into(), - ), - ("punctuation.special".into(), red().dark().step_11().into()), - ("string".into(), lime().dark().step_11().into()), - ("string.escape".into(), orange().dark().step_11().into()), - ("string.regex".into(), tomato().dark().step_11().into()), - ("string.special".into(), red().dark().step_11().into()), - ( - "string.special.symbol".into(), - red().dark().step_11().into(), - ), - ("tag".into(), red().dark().step_11().into()), - ("text.literal".into(), purple().dark().step_11().into()), - ("title".into(), sky().dark().step_11().into()), - ("type".into(), mint().dark().step_11().into()), - ("variable".into(), red().dark().step_11().into()), - ("variable.special".into(), red().dark().step_11().into()), - ("variant".into(), red().dark().step_11().into()), - ], - inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style") - suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style") - } - } -} - impl ThemeColors { - pub fn default_light() -> Self { + pub fn light() -> Self { let system = SystemColors::default(); Self { @@ -309,7 +81,7 @@ impl ThemeColors { } } - pub fn default_dark() -> Self { + pub fn dark() -> Self { let system = SystemColors::default(); Self { diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 4e8caf67b1..95a95c687f 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::{ - colors::{StatusColors, SystemColors, ThemeColors, ThemeStyles}, - default_color_scales, Appearance, PlayerColors, SyntaxTheme, Theme, ThemeFamily, + default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, + ThemeColors, ThemeFamily, ThemeStyles, }; fn zed_pro_daylight() -> Theme { @@ -12,10 +12,10 @@ fn zed_pro_daylight() -> Theme { appearance: Appearance::Light, styles: ThemeStyles { system: SystemColors::default(), - colors: ThemeColors::default_light(), + colors: ThemeColors::light(), status: StatusColors::light(), - player: PlayerColors::default_light(), - syntax: Arc::new(SyntaxTheme::default_light()), + player: PlayerColors::light(), + syntax: Arc::new(SyntaxTheme::light()), }, } } @@ -27,10 +27,10 @@ pub(crate) fn zed_pro_moonlight() -> Theme { appearance: Appearance::Dark, styles: ThemeStyles { system: SystemColors::default(), - colors: ThemeColors::default_dark(), + colors: ThemeColors::dark(), status: StatusColors::dark(), - player: PlayerColors::default(), - syntax: Arc::new(SyntaxTheme::default_dark()), + player: PlayerColors::dark(), + syntax: Arc::new(SyntaxTheme::dark()), }, } } diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index a28c59b6e1..c8773ea08b 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -38,8 +38,8 @@ impl ThemeRegistry { fn insert_user_themes(&mut self, themes: impl IntoIterator) { self.insert_themes(themes.into_iter().map(|user_theme| { let mut theme_colors = match user_theme.appearance { - Appearance::Light => ThemeColors::default_light(), - Appearance::Dark => ThemeColors::default_dark(), + Appearance::Light => ThemeColors::light(), + Appearance::Dark => ThemeColors::dark(), }; theme_colors.refine(&user_theme.styles.colors); @@ -47,8 +47,8 @@ impl ThemeRegistry { status_colors.refine(&user_theme.styles.status); let mut syntax_colors = match user_theme.appearance { - Appearance::Light => SyntaxTheme::default_light(), - Appearance::Dark => SyntaxTheme::default_dark(), + Appearance::Light => SyntaxTheme::light(), + Appearance::Dark => SyntaxTheme::dark(), }; if let Some(user_syntax) = user_theme.styles.syntax { syntax_colors.highlights = user_syntax diff --git a/crates/theme2/src/styles/colors.rs b/crates/theme2/src/styles/colors.rs index 3104f46705..1d4917ac00 100644 --- a/crates/theme2/src/styles/colors.rs +++ b/crates/theme2/src/styles/colors.rs @@ -2,54 +2,7 @@ use gpui::Hsla; use refineable::Refineable; use std::sync::Arc; -use crate::{PlayerColors, SyntaxTheme, SystemColors}; - -#[derive(Refineable, Clone, Debug)] -#[refineable(Debug, serde::Deserialize)] -pub struct StatusColors { - /// Indicates some kind of conflict, like a file changed on disk while it was open, or - /// merge conflicts in a Git repository. - pub conflict: Hsla, - - /// Indicates something new, like a new file added to a Git repository. - pub created: Hsla, - - /// Indicates that something no longer exists, like a deleted file. - pub deleted: Hsla, - - /// Indicates a system error, a failed operation or a diagnostic error. - pub error: Hsla, - - /// Represents a hidden status, such as a file being hidden in a file tree. - pub hidden: Hsla, - - /// Indicates a hint or some kind of additional information. - pub hint: Hsla, - - /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. - pub ignored: Hsla, - - /// Represents informational status updates or messages. - pub info: Hsla, - - /// Indicates a changed or altered status, like a file that has been edited. - pub modified: Hsla, - - /// Indicates something that is predicted, like automatic code completion, or generated code. - pub predictive: Hsla, - - /// Represents a renamed status, such as a file that has been renamed. - pub renamed: Hsla, - - /// Indicates a successful operation or task completion. - pub success: Hsla, - - /// Indicates some kind of unreachable status, like a block of code that can never be reached. - pub unreachable: Hsla, - - /// Represents a warning status, like an operation that is about to fail. - pub warning: Hsla, -} +use crate::{PlayerColors, StatusColors, SyntaxTheme, SystemColors}; #[derive(Refineable, Clone, Debug)] #[refineable(Debug, serde::Deserialize)] @@ -283,7 +236,7 @@ mod tests { #[test] fn override_a_single_theme_color() { - let mut colors = ThemeColors::default_light(); + let mut colors = ThemeColors::light(); let magenta: Hsla = gpui::rgb(0xff00ff); @@ -301,7 +254,7 @@ mod tests { #[test] fn override_multiple_theme_colors() { - let mut colors = ThemeColors::default_light(); + let mut colors = ThemeColors::light(); let magenta: Hsla = gpui::rgb(0xff00ff); let green: Hsla = gpui::rgb(0x00ff00); diff --git a/crates/theme2/src/styles/players.rs b/crates/theme2/src/styles/players.rs index 0e36ff5947..68deceb0ff 100644 --- a/crates/theme2/src/styles/players.rs +++ b/crates/theme2/src/styles/players.rs @@ -16,6 +16,107 @@ pub struct PlayerColor { #[derive(Clone)] pub struct PlayerColors(pub Vec); +impl Default for PlayerColors { + /// Don't use this! + /// We have to have a default to be `[refineable::Refinable]`. + /// todo!("Find a way to not need this for Refinable") + fn default() -> Self { + Self::dark() + } +} + +impl PlayerColors { + pub fn dark() -> Self { + Self(vec![ + PlayerColor { + cursor: blue().dark().step_9(), + background: blue().dark().step_5(), + selection: blue().dark().step_3(), + }, + PlayerColor { + cursor: orange().dark().step_9(), + background: orange().dark().step_5(), + selection: orange().dark().step_3(), + }, + PlayerColor { + cursor: pink().dark().step_9(), + background: pink().dark().step_5(), + selection: pink().dark().step_3(), + }, + PlayerColor { + cursor: lime().dark().step_9(), + background: lime().dark().step_5(), + selection: lime().dark().step_3(), + }, + PlayerColor { + cursor: purple().dark().step_9(), + background: purple().dark().step_5(), + selection: purple().dark().step_3(), + }, + PlayerColor { + cursor: amber().dark().step_9(), + background: amber().dark().step_5(), + selection: amber().dark().step_3(), + }, + PlayerColor { + cursor: jade().dark().step_9(), + background: jade().dark().step_5(), + selection: jade().dark().step_3(), + }, + PlayerColor { + cursor: red().dark().step_9(), + background: red().dark().step_5(), + selection: red().dark().step_3(), + }, + ]) + } + + pub fn light() -> Self { + Self(vec![ + PlayerColor { + cursor: blue().light().step_9(), + background: blue().light().step_4(), + selection: blue().light().step_3(), + }, + PlayerColor { + cursor: orange().light().step_9(), + background: orange().light().step_4(), + selection: orange().light().step_3(), + }, + PlayerColor { + cursor: pink().light().step_9(), + background: pink().light().step_4(), + selection: pink().light().step_3(), + }, + PlayerColor { + cursor: lime().light().step_9(), + background: lime().light().step_4(), + selection: lime().light().step_3(), + }, + PlayerColor { + cursor: purple().light().step_9(), + background: purple().light().step_4(), + selection: purple().light().step_3(), + }, + PlayerColor { + cursor: amber().light().step_9(), + background: amber().light().step_4(), + selection: amber().light().step_3(), + }, + PlayerColor { + cursor: jade().light().step_9(), + background: jade().light().step_4(), + selection: jade().light().step_3(), + }, + PlayerColor { + cursor: red().light().step_9(), + background: red().light().step_4(), + selection: red().light().step_3(), + }, + ]) + } +} + impl PlayerColors { pub fn local(&self) -> PlayerColor { // todo!("use a valid color"); @@ -36,6 +137,8 @@ impl PlayerColors { #[cfg(feature = "stories")] pub use stories::*; +use crate::{amber, blue, jade, lime, orange, pink, purple, red}; + #[cfg(feature = "stories")] mod stories { use super::*; diff --git a/crates/theme2/src/styles/status.rs b/crates/theme2/src/styles/status.rs index cf2a1b64f6..db0f475825 100644 --- a/crates/theme2/src/styles/status.rs +++ b/crates/theme2/src/styles/status.rs @@ -1,10 +1,59 @@ use gpui::Hsla; +use refineable::Refineable; -use crate::{blue, grass, neutral, red, yellow, StatusColors}; +use crate::{blue, grass, neutral, red, yellow}; + +#[derive(Refineable, Clone, Debug)] +#[refineable(Debug, serde::Deserialize)] +pub struct StatusColors { + /// Indicates some kind of conflict, like a file changed on disk while it was open, or + /// merge conflicts in a Git repository. + pub conflict: Hsla, + + /// Indicates something new, like a new file added to a Git repository. + pub created: Hsla, + + /// Indicates that something no longer exists, like a deleted file. + pub deleted: Hsla, + + /// Indicates a system error, a failed operation or a diagnostic error. + pub error: Hsla, + + /// Represents a hidden status, such as a file being hidden in a file tree. + pub hidden: Hsla, + + /// Indicates a hint or some kind of additional information. + pub hint: Hsla, + + /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. + pub ignored: Hsla, + + /// Represents informational status updates or messages. + pub info: Hsla, + + /// Indicates a changed or altered status, like a file that has been edited. + pub modified: Hsla, + + /// Indicates something that is predicted, like automatic code completion, or generated code. + pub predictive: Hsla, + + /// Represents a renamed status, such as a file that has been renamed. + pub renamed: Hsla, + + /// Indicates a successful operation or task completion. + pub success: Hsla, + + /// Indicates some kind of unreachable status, like a block of code that can never be reached. + pub unreachable: Hsla, + + /// Represents a warning status, like an operation that is about to fail. + pub warning: Hsla, +} impl Default for StatusColors { /// Don't use this! - /// We have to have a default for StatusColors to be `[refineable::Refinable]`. + /// We have to have a default to be `[refineable::Refinable]`. + /// todo!("Find a way to not need this for Refinable") fn default() -> Self { Self::dark() } diff --git a/crates/theme2/src/styles/syntax.rs b/crates/theme2/src/styles/syntax.rs index 8aac238555..8675d30e3a 100644 --- a/crates/theme2/src/styles/syntax.rs +++ b/crates/theme2/src/styles/syntax.rs @@ -1,13 +1,142 @@ use gpui::{HighlightStyle, Hsla}; +use crate::{ + blue, cyan, gold, indigo, iris, jade, lime, mint, neutral, orange, plum, purple, red, sky, + tomato, yellow, +}; + #[derive(Clone, Default)] pub struct SyntaxTheme { pub highlights: Vec<(String, HighlightStyle)>, + // todo!("Remove this in favor of StatusColor.hint") + // If this should be overridable we should move it to ThemeColors pub inlay_style: HighlightStyle, + // todo!("Remove this in favor of StatusColor.prediction") + // If this should be overridable we should move it to ThemeColors pub suggestion_style: HighlightStyle, } impl SyntaxTheme { + pub fn light() -> Self { + Self { + highlights: vec![ + ("attribute".into(), cyan().light().step_11().into()), + ("boolean".into(), tomato().light().step_11().into()), + ("comment".into(), neutral().light().step_11().into()), + ("comment.doc".into(), iris().light().step_12().into()), + ("constant".into(), red().light().step_9().into()), + ("constructor".into(), red().light().step_9().into()), + ("embedded".into(), red().light().step_9().into()), + ("emphasis".into(), red().light().step_9().into()), + ("emphasis.strong".into(), red().light().step_9().into()), + ("enum".into(), red().light().step_9().into()), + ("function".into(), red().light().step_9().into()), + ("hint".into(), red().light().step_9().into()), + ("keyword".into(), orange().light().step_11().into()), + ("label".into(), red().light().step_9().into()), + ("link_text".into(), red().light().step_9().into()), + ("link_uri".into(), red().light().step_9().into()), + ("number".into(), red().light().step_9().into()), + ("operator".into(), red().light().step_9().into()), + ("predictive".into(), red().light().step_9().into()), + ("preproc".into(), red().light().step_9().into()), + ("primary".into(), red().light().step_9().into()), + ("property".into(), red().light().step_9().into()), + ("punctuation".into(), neutral().light().step_11().into()), + ( + "punctuation.bracket".into(), + neutral().light().step_11().into(), + ), + ( + "punctuation.delimiter".into(), + neutral().light().step_11().into(), + ), + ( + "punctuation.list_marker".into(), + blue().light().step_11().into(), + ), + ("punctuation.special".into(), red().light().step_9().into()), + ("string".into(), jade().light().step_11().into()), + ("string.escape".into(), red().light().step_9().into()), + ("string.regex".into(), tomato().light().step_11().into()), + ("string.special".into(), red().light().step_9().into()), + ( + "string.special.symbol".into(), + red().light().step_9().into(), + ), + ("tag".into(), red().light().step_9().into()), + ("text.literal".into(), red().light().step_9().into()), + ("title".into(), red().light().step_9().into()), + ("type".into(), red().light().step_9().into()), + ("variable".into(), red().light().step_9().into()), + ("variable.special".into(), red().light().step_9().into()), + ("variant".into(), red().light().step_9().into()), + ], + inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style") + suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style") + } + } + + pub fn dark() -> Self { + Self { + highlights: vec![ + ("attribute".into(), tomato().dark().step_11().into()), + ("boolean".into(), tomato().dark().step_11().into()), + ("comment".into(), neutral().dark().step_11().into()), + ("comment.doc".into(), iris().dark().step_12().into()), + ("constant".into(), orange().dark().step_11().into()), + ("constructor".into(), gold().dark().step_11().into()), + ("embedded".into(), red().dark().step_11().into()), + ("emphasis".into(), red().dark().step_11().into()), + ("emphasis.strong".into(), red().dark().step_11().into()), + ("enum".into(), yellow().dark().step_11().into()), + ("function".into(), blue().dark().step_11().into()), + ("hint".into(), indigo().dark().step_11().into()), + ("keyword".into(), plum().dark().step_11().into()), + ("label".into(), red().dark().step_11().into()), + ("link_text".into(), red().dark().step_11().into()), + ("link_uri".into(), red().dark().step_11().into()), + ("number".into(), red().dark().step_11().into()), + ("operator".into(), red().dark().step_11().into()), + ("predictive".into(), red().dark().step_11().into()), + ("preproc".into(), red().dark().step_11().into()), + ("primary".into(), red().dark().step_11().into()), + ("property".into(), red().dark().step_11().into()), + ("punctuation".into(), neutral().dark().step_11().into()), + ( + "punctuation.bracket".into(), + neutral().dark().step_11().into(), + ), + ( + "punctuation.delimiter".into(), + neutral().dark().step_11().into(), + ), + ( + "punctuation.list_marker".into(), + blue().dark().step_11().into(), + ), + ("punctuation.special".into(), red().dark().step_11().into()), + ("string".into(), lime().dark().step_11().into()), + ("string.escape".into(), orange().dark().step_11().into()), + ("string.regex".into(), tomato().dark().step_11().into()), + ("string.special".into(), red().dark().step_11().into()), + ( + "string.special.symbol".into(), + red().dark().step_11().into(), + ), + ("tag".into(), red().dark().step_11().into()), + ("text.literal".into(), purple().dark().step_11().into()), + ("title".into(), sky().dark().step_11().into()), + ("type".into(), mint().dark().step_11().into()), + ("variable".into(), red().dark().step_11().into()), + ("variable.special".into(), red().dark().step_11().into()), + ("variant".into(), red().dark().step_11().into()), + ], + inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style") + suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style") + } + } + // TOOD: Get this working with `#[cfg(test)]`. Why isn't it? pub fn new_test(colors: impl IntoIterator) -> Self { SyntaxTheme { diff --git a/crates/theme2/src/styles/system.rs b/crates/theme2/src/styles/system.rs index 6a7ca101e2..aeb0865155 100644 --- a/crates/theme2/src/styles/system.rs +++ b/crates/theme2/src/styles/system.rs @@ -1,4 +1,4 @@ -use gpui::Hsla; +use gpui::{hsla, Hsla}; #[derive(Clone)] pub struct SystemColors { @@ -7,3 +7,14 @@ pub struct SystemColors { pub mac_os_traffic_light_yellow: Hsla, pub mac_os_traffic_light_green: Hsla, } + +impl Default for SystemColors { + fn default() -> Self { + Self { + transparent: hsla(0.0, 0.0, 0.0, 0.0), + mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0), + mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0), + mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0), + } + } +} diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index 0d6600eca6..06d132d39d 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -1,12 +1,9 @@ -mod colors; mod default_colors; mod default_theme; -mod players; mod registry; mod scale; mod settings; mod styles; -mod syntax; #[cfg(not(feature = "importing-themes"))] mod themes; mod user_theme; @@ -14,15 +11,12 @@ mod user_theme; use std::sync::Arc; use ::settings::Settings; -pub use colors::*; pub use default_colors::*; pub use default_theme::*; -pub use players::*; pub use registry::*; pub use scale::*; pub use settings::*; pub use styles::*; -pub use syntax::*; #[cfg(not(feature = "importing-themes"))] pub use themes::*; pub use user_theme::*; From 91b634fc59ef7f0ed52ac7a23d345505a0de64b7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 13 Nov 2023 11:08:51 -0800 Subject: [PATCH 035/126] Get some project panel actions working Add styling for hovered and selected items Co-authored-by: Marshall Co-authored-by: Julia --- crates/project_panel2/src/project_panel.rs | 88 ++++++++++++---------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index fec44e3d68..22d9d31336 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -8,11 +8,11 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ - actions, div, px, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, - AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusEnabled, + actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, + AsyncWindowContext, ClipboardItem, Component, Div, Element, Entity, EventEmitter, FocusEnabled, FocusHandle, Model, ParentElement as _, Pixels, Point, PromptLevel, Render, - StatefulInteractive, StatefulInteractivity, Styled, Task, UniformListScrollHandle, View, - ViewContext, VisualContext as _, WeakView, WindowContext, + StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Task, + UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -132,25 +132,6 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); file_associations::init(assets, cx); - // cx.add_action(ProjectPanel::expand_selected_entry); - // cx.add_action(ProjectPanel::collapse_selected_entry); - // cx.add_action(ProjectPanel::collapse_all_entries); - // cx.add_action(ProjectPanel::select_prev); - // cx.add_action(ProjectPanel::select_next); - // cx.add_action(ProjectPanel::new_file); - // cx.add_action(ProjectPanel::new_directory); - // cx.add_action(ProjectPanel::rename); - // cx.add_async_action(ProjectPanel::delete); - // cx.add_async_action(ProjectPanel::confirm); - // cx.add_async_action(ProjectPanel::open_file); - // cx.add_action(ProjectPanel::cancel); - // cx.add_action(ProjectPanel::cut); - // cx.add_action(ProjectPanel::copy); - // cx.add_action(ProjectPanel::copy_path); - // cx.add_action(ProjectPanel::copy_relative_path); - // cx.add_action(ProjectPanel::reveal_in_finder); - // cx.add_action(ProjectPanel::open_in_terminal); - // cx.add_action(ProjectPanel::new_search_in_directory); // cx.add_action( // |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { // this.paste(action, cx); @@ -1366,15 +1347,22 @@ impl ProjectPanel { .git_status .as_ref() .map(|status| match status { - GitFileStatus::Added => theme.styles.status.created, - GitFileStatus::Modified => theme.styles.status.modified, - GitFileStatus::Conflict => theme.styles.status.conflict, + GitFileStatus::Added => theme.status().created, + GitFileStatus::Modified => theme.status().modified, + GitFileStatus::Conflict => theme.status().conflict, }) - .unwrap_or(theme.styles.status.info); + .unwrap_or(theme.status().info); h_stack() .child(if let Some(icon) = &details.icon { - div().child(svg().path(icon.to_string())) + div().child( + // todo!() Marshall: Can we use our `IconElement` component here? + svg() + .size(rems(0.9375)) + .flex_none() + .path(icon.to_string()) + .text_color(cx.theme().colors().icon), + ) } else { div() }) @@ -1390,11 +1378,10 @@ impl ProjectPanel { } fn render_entry( + &self, entry_id: ProjectEntryId, details: EntryDetails, - editor: &View, // dragged_entry_destination: &mut Option>, - // theme: &theme::ProjectPanel, cx: &mut ViewContext, ) -> Div> { let kind = details.kind; @@ -1402,9 +1389,18 @@ impl ProjectPanel { const INDENT_SIZE: Pixels = px(16.0); let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); let show_editor = details.is_editing && !details.is_processing; + let is_selected = self + .selection + .map_or(false, |selection| selection.entry_id == entry_id); - Self::render_entry_visual_element(&details, Some(editor), padding, cx) + Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx) .id(entry_id.to_proto() as usize) + .w_full() + .cursor_pointer() + .when(is_selected, |this| { + this.bg(cx.theme().colors().element_selected) + }) + .hover(|style| style.bg(cx.theme().colors().element_hover)) .on_click(move |this, event, cx| { if !show_editor { if kind.is_dir() { @@ -1441,7 +1437,6 @@ impl Render for ProjectPanel { type Element = Div, FocusEnabled>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { - enum ProjectPanel {} let theme = cx.theme(); let last_worktree_root_id = self.last_worktree_root_id; @@ -1449,8 +1444,28 @@ impl Render for ProjectPanel { if has_worktree { div() - .size_full() .id("project-panel") + .size_full() + .context("ProjectPanel") + .on_action(Self::select_next) + .on_action(Self::select_prev) + .on_action(Self::expand_selected_entry) + .on_action(Self::collapse_selected_entry) + .on_action(Self::collapse_all_entries) + .on_action(Self::new_file) + .on_action(Self::new_directory) + .on_action(Self::rename) + // .on_action(Self::delete) + // .on_action(Self::confirm) + // .on_action(Self::open_file) + .on_action(Self::cancel) + .on_action(Self::cut) + .on_action(Self::copy) + .on_action(Self::copy_path) + .on_action(Self::copy_relative_path) + .on_action(Self::reveal_in_finder) + .on_action(Self::open_in_terminal) + .on_action(Self::new_search_in_directory) .track_focus(&self.focus_handle) .child( uniform_list( @@ -1462,11 +1477,8 @@ impl Render for ProjectPanel { |this: &mut Self, range, cx| { let mut items = SmallVec::new(); this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(Self::render_entry( - id, - details, - &this.filename_editor, - // &mut dragged_entry_destination, + items.push(this.render_entry( + id, details, // &mut dragged_entry_destination, cx, )); }); From c0f34e33029ec1f411074628397552bc697cb93e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 13 Nov 2023 11:38:47 -0800 Subject: [PATCH 036/126] Add delete, open file, and rename actions to the project panel Co-authored-by: Julia Co-authored-by: Marshall --- crates/project_panel2/src/project_panel.rs | 96 +++++++++++----------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 22d9d31336..b963625978 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -8,11 +8,11 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ - actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, - AsyncWindowContext, ClipboardItem, Component, Div, Element, Entity, EventEmitter, FocusEnabled, - FocusHandle, Model, ParentElement as _, Pixels, Point, PromptLevel, Render, - StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Task, - UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, + actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, + ClipboardItem, Component, Div, Entity, EventEmitter, FocusEnabled, FocusHandle, Model, + ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, + StatefulInteractivity, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, + ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -33,7 +33,7 @@ use std::{ use theme::ActiveTheme as _; use ui::{h_stack, v_stack, Label}; use unicase::UniCase; -use util::TryFutureExt; +use util::{maybe, TryFutureExt}; use workspace::{ dock::{DockPosition, PanelEvent}, Workspace, @@ -131,12 +131,6 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); file_associations::init(assets, cx); - - // cx.add_action( - // |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { - // this.paste(action, cx); - // }, - // ); } #[derive(Debug)] @@ -560,22 +554,18 @@ impl ProjectPanel { } } - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(task) = self.confirm_edit(cx) { - return Some(task); + task.detach_and_log_err(cx); } - - None } - fn open_file(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { + fn open_file(&mut self, _: &Open, cx: &mut ViewContext) { if let Some((_, entry)) = self.selected_entry(cx) { if entry.is_file() { self.open_entry(entry.id, true, cx); } } - - None } fn confirm_edit(&mut self, cx: &mut ViewContext) -> Option>> { @@ -781,27 +771,32 @@ impl ProjectPanel { } } - fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { - let Selection { entry_id, .. } = self.selection?; - let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; - let file_name = path.file_name()?; + fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { + maybe!({ + let Selection { entry_id, .. } = self.selection?; + let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; + let file_name = path.file_name()?; - let mut answer = cx.prompt( - PromptLevel::Info, - &format!("Delete {file_name:?}?"), - &["Delete", "Cancel"], - ); - Some(cx.spawn(|this, mut cx| async move { - if answer.await != Ok(0) { - return Ok(()); - } - this.update(&mut cx, |this, cx| { - this.project - .update(cx, |project, cx| project.delete_entry(entry_id, cx)) - .ok_or_else(|| anyhow!("no such entry")) - })?? - .await - })) + let answer = cx.prompt( + PromptLevel::Info, + &format!("Delete {file_name:?}?"), + &["Delete", "Cancel"], + ); + + cx.spawn(|this, mut cx| async move { + if answer.await != Ok(0) { + return Ok(()); + } + this.update(&mut cx, |this, cx| { + this.project + .update(cx, |project, cx| project.delete_entry(entry_id, cx)) + .ok_or_else(|| anyhow!("no such entry")) + })?? + .await + }) + .detach_and_log_err(cx); + Some(()) + }); } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { @@ -878,8 +873,9 @@ impl ProjectPanel { } } - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) -> Option<()> { - if let Some((worktree, entry)) = self.selected_entry(cx) { + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + maybe!({ + let (worktree, entry) = self.selected_entry(cx)?; let clipboard_entry = self.clipboard_entry?; if clipboard_entry.worktree_id() != worktree.id() { return None; @@ -923,15 +919,16 @@ impl ProjectPanel { if let Some(task) = self.project.update(cx, |project, cx| { project.rename_entry(clipboard_entry.entry_id(), new_path, cx) }) { - task.detach_and_log_err(cx) + task.detach_and_log_err(cx); } } else if let Some(task) = self.project.update(cx, |project, cx| { project.copy_entry(clipboard_entry.entry_id(), new_path, cx) }) { - task.detach_and_log_err(cx) + task.detach_and_log_err(cx); } - } - None + + Some(()) + }); } fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { @@ -1368,7 +1365,7 @@ impl ProjectPanel { }) .child( if let (Some(editor), true) = (editor, show_editor) { - div().child(editor.clone()) + div().w_full().child(editor.clone()) } else { div().child(Label::new(details.filename.clone())) } @@ -1455,14 +1452,15 @@ impl Render for ProjectPanel { .on_action(Self::new_file) .on_action(Self::new_directory) .on_action(Self::rename) - // .on_action(Self::delete) - // .on_action(Self::confirm) - // .on_action(Self::open_file) + .on_action(Self::delete) + .on_action(Self::confirm) + .on_action(Self::open_file) .on_action(Self::cancel) .on_action(Self::cut) .on_action(Self::copy) .on_action(Self::copy_path) .on_action(Self::copy_relative_path) + .on_action(Self::paste) .on_action(Self::reveal_in_finder) .on_action(Self::open_in_terminal) .on_action(Self::new_search_in_directory) From 7e7b06553540594feee9e654571f89a079ea37fe Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 12:48:36 -0700 Subject: [PATCH 037/126] Fix on_action on focusable We were accidentally dropping the key context --- crates/go_to_line2/src/go_to_line.rs | 7 ++-- crates/gpui2/src/elements/div.rs | 6 +-- crates/gpui2/src/interactive.rs | 5 ++- crates/gpui2/src/key_dispatch.rs | 55 ++++++++++------------------ crates/gpui2/src/window.rs | 6 +-- crates/picker2/src/picker2.rs | 9 ++--- 6 files changed, 34 insertions(+), 54 deletions(-) diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 9ec770e05c..50592901b5 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,8 +1,7 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString, - StatefulInteractivity, StatelessInteractive, Styled, Subscription, View, ViewContext, - VisualContext, WindowContext, + StatelessInteractive, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; @@ -146,11 +145,11 @@ impl GoToLine { } impl Render for GoToLine { - type Element = Div>; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { modal(cx) - .id("go to line") + .context("GoToLine") .on_action(Self::cancel) .on_action(Self::confirm) .w_96() diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 7bfd4b244a..95c44038ed 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -128,7 +128,7 @@ impl Div, NonFocusableKeyDispatch> { pub fn focusable(self) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity, - key_dispatch: FocusableKeyDispatch::new(), + key_dispatch: FocusableKeyDispatch::new(self.key_dispatch), children: self.children, group: self.group, base_style: self.base_style, @@ -141,7 +141,7 @@ impl Div, NonFocusableKeyDispatch> { ) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity, - key_dispatch: FocusableKeyDispatch::tracked(handle), + key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle), children: self.children, group: self.group, base_style: self.base_style, @@ -172,7 +172,7 @@ impl Div, NonFocusableKeyDispatch> { ) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity.into_stateful(handle), - key_dispatch: handle.clone().into(), + key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle), children: self.children, group: self.group, base_style: self.base_style, diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 4a7633f8dc..aacaeac01f 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1247,9 +1247,10 @@ mod test { fn render(&mut self, _: &mut gpui::ViewContext) -> Self::Element { div().id("testview").child( div() + .context("test") + .track_focus(&self.focus_handle) .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true) - .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true) - .track_focus(&self.focus_handle), + .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true), ) } } diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index e517b8d34b..8ace4188ae 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -33,7 +33,7 @@ pub(crate) struct DispatchTree { #[derive(Default)] pub(crate) struct DispatchNode { pub key_listeners: SmallVec<[KeyListener; 2]>, - pub action_listeners: SmallVec<[ActionListener; 16]>, + pub action_listeners: SmallVec<[DispatchActionListener; 16]>, pub context: KeyContext, parent: Option, } @@ -41,7 +41,7 @@ pub(crate) struct DispatchNode { type KeyListener = Rc; #[derive(Clone)] -pub(crate) struct ActionListener { +pub(crate) struct DispatchActionListener { pub(crate) action_type: TypeId, pub(crate) listener: Rc, } @@ -102,10 +102,12 @@ impl DispatchTree { action_type: TypeId, listener: Rc, ) { - self.active_node().action_listeners.push(ActionListener { - action_type, - listener, - }); + self.active_node() + .action_listeners + .push(DispatchActionListener { + action_type, + listener, + }); } pub fn make_focusable(&mut self, focus_id: FocusId) { @@ -135,7 +137,7 @@ impl DispatchTree { if let Some(node) = self.focusable_node_ids.get(&target) { for node_id in self.dispatch_path(*node) { let node = &self.nodes[node_id.0]; - for ActionListener { action_type, .. } in &node.action_listeners { + for DispatchActionListener { action_type, .. } in &node.action_listeners { actions.extend(build_action_from_type(action_type).log_err()); } } @@ -148,21 +150,15 @@ impl DispatchTree { keystroke: &Keystroke, context: &[KeyContext], ) -> Option> { - if !self - .keystroke_matchers - .contains_key(self.context_stack.as_slice()) - { - let keystroke_contexts = self.context_stack.iter().cloned().collect(); + if !self.keystroke_matchers.contains_key(context) { + let keystroke_contexts = context.iter().cloned().collect(); self.keystroke_matchers.insert( keystroke_contexts, KeystrokeMatcher::new(self.keymap.clone()), ); } - let keystroke_matcher = self - .keystroke_matchers - .get_mut(self.context_stack.as_slice()) - .unwrap(); + let keystroke_matcher = self.keystroke_matchers.get_mut(context).unwrap(); if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke(keystroke, context) { // Clear all pending keystrokes when an action has been found. for keystroke_matcher in self.keystroke_matchers.values_mut() { @@ -274,7 +270,7 @@ pub trait KeyDispatch: 'static { } pub struct FocusableKeyDispatch { - pub key_context: KeyContext, + pub non_focusable: NonFocusableKeyDispatch, pub focus_handle: Option, pub focus_listeners: FocusListeners, pub focus_style: StyleRefinement, @@ -283,9 +279,9 @@ pub struct FocusableKeyDispatch { } impl FocusableKeyDispatch { - pub fn new() -> Self { + pub fn new(non_focusable: NonFocusableKeyDispatch) -> Self { Self { - key_context: KeyContext::default(), + non_focusable, focus_handle: None, focus_listeners: FocusListeners::default(), focus_style: StyleRefinement::default(), @@ -294,9 +290,9 @@ impl FocusableKeyDispatch { } } - pub fn tracked(handle: &FocusHandle) -> Self { + pub fn tracked(non_focusable: NonFocusableKeyDispatch, handle: &FocusHandle) -> Self { Self { - key_context: KeyContext::default(), + non_focusable, focus_handle: Some(handle.clone()), focus_listeners: FocusListeners::default(), focus_style: StyleRefinement::default(), @@ -316,24 +312,11 @@ impl KeyDispatch for FocusableKeyDispatch { } fn key_context(&self) -> &KeyContext { - &self.key_context + &self.non_focusable.key_context } fn key_context_mut(&mut self) -> &mut KeyContext { - &mut self.key_context - } -} - -impl From for FocusableKeyDispatch { - fn from(value: FocusHandle) -> Self { - Self { - key_context: KeyContext::default(), - focus_handle: Some(value), - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } + &mut self.non_focusable.key_context } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 11878c15fa..4a7241a5c5 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,5 +1,5 @@ use crate::{ - key_dispatch::ActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, + key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, @@ -1306,7 +1306,7 @@ impl<'a> WindowContext<'a> { // Capture phase for node_id in &dispatch_path { let node = self.window.current_frame.dispatch_tree.node(*node_id); - for ActionListener { + for DispatchActionListener { action_type, listener, } in node.action_listeners.clone() @@ -1324,7 +1324,7 @@ impl<'a> WindowContext<'a> { // Bubble phase for node_id in dispatch_path.iter().rev() { let node = self.window.current_frame.dispatch_tree.node(*node_id); - for ActionListener { + for DispatchActionListener { action_type, listener, } in node.action_listeners.clone() diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 62c5308dec..9d75fcb890 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,8 +1,7 @@ use editor::Editor; use gpui::{ - div, uniform_list, Component, Div, FocusableKeyDispatch, ParentElement, Render, - StatefulInteractivity, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, - ViewContext, VisualContext, WindowContext, + div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled, Task, + UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; use std::cmp; use theme::ActiveTheme; @@ -137,13 +136,11 @@ impl Picker { } impl Render for Picker { - type Element = Div, FocusableKeyDispatch>; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .context("picker") - .id("picker-container") - .focusable() .size_full() .on_action(Self::select_next) .on_action(Self::select_prev) From 04ad19d01bcddbe7f2cc0f44eeb95b27e4ee1f9a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 13 Nov 2023 15:07:13 -0500 Subject: [PATCH 038/126] Choose appropriate player colors based on theme appearance --- crates/theme2/src/registry.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index c8773ea08b..5f576e51f0 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -76,7 +76,10 @@ impl ThemeRegistry { system: SystemColors::default(), colors: theme_colors, status: status_colors, - player: PlayerColors::default(), + player: match user_theme.appearance { + Appearance::Light => PlayerColors::light(), + Appearance::Dark => PlayerColors::dark(), + }, syntax: Arc::new(syntax_colors), }, } From 0e3fd92bd047e2f9a00c2f02e4e09bbdcab48778 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 13 Nov 2023 12:10:14 -0800 Subject: [PATCH 039/126] Get editor tests compiling --- crates/editor2/src/editor.rs | 120 +- crates/editor2/src/editor_tests.rs | 2604 +++++++++-------- .../src/test/editor_lsp_test_context.rs | 18 +- .../editor2/src/test/editor_test_context.rs | 129 +- crates/gpui2/src/app/test_context.rs | 82 +- crates/gpui2/src/color.rs | 31 +- crates/gpui2/src/util.rs | 26 +- crates/workspace2/src/workspace2.rs | 34 +- 8 files changed, 1618 insertions(+), 1426 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index b1f0d26786..891c15580e 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -10056,76 +10056,76 @@ pub fn diagnostic_style( } } -// pub fn combine_syntax_and_fuzzy_match_highlights( -// text: &str, -// default_style: HighlightStyle, -// syntax_ranges: impl Iterator, HighlightStyle)>, -// match_indices: &[usize], -// ) -> Vec<(Range, HighlightStyle)> { -// let mut result = Vec::new(); -// let mut match_indices = match_indices.iter().copied().peekable(); +pub fn combine_syntax_and_fuzzy_match_highlights( + text: &str, + default_style: HighlightStyle, + syntax_ranges: impl Iterator, HighlightStyle)>, + match_indices: &[usize], +) -> Vec<(Range, HighlightStyle)> { + let mut result = Vec::new(); + let mut match_indices = match_indices.iter().copied().peekable(); -// for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) -// { -// syntax_highlight.weight = None; + for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) + { + syntax_highlight.font_weight = None; -// // Add highlights for any fuzzy match characters before the next -// // syntax highlight range. -// while let Some(&match_index) = match_indices.peek() { -// if match_index >= range.start { -// break; -// } -// match_indices.next(); -// let end_index = char_ix_after(match_index, text); -// let mut match_style = default_style; -// match_style.weight = Some(FontWeight::BOLD); -// result.push((match_index..end_index, match_style)); -// } + // Add highlights for any fuzzy match characters before the next + // syntax highlight range. + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.start { + break; + } + match_indices.next(); + let end_index = char_ix_after(match_index, text); + let mut match_style = default_style; + match_style.font_weight = Some(FontWeight::BOLD); + result.push((match_index..end_index, match_style)); + } -// if range.start == usize::MAX { -// break; -// } + if range.start == usize::MAX { + break; + } -// // Add highlights for any fuzzy match characters within the -// // syntax highlight range. -// let mut offset = range.start; -// while let Some(&match_index) = match_indices.peek() { -// if match_index >= range.end { -// break; -// } + // Add highlights for any fuzzy match characters within the + // syntax highlight range. + let mut offset = range.start; + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.end { + break; + } -// match_indices.next(); -// if match_index > offset { -// result.push((offset..match_index, syntax_highlight)); -// } + match_indices.next(); + if match_index > offset { + result.push((offset..match_index, syntax_highlight)); + } -// let mut end_index = char_ix_after(match_index, text); -// while let Some(&next_match_index) = match_indices.peek() { -// if next_match_index == end_index && next_match_index < range.end { -// end_index = char_ix_after(next_match_index, text); -// match_indices.next(); -// } else { -// break; -// } -// } + let mut end_index = char_ix_after(match_index, text); + while let Some(&next_match_index) = match_indices.peek() { + if next_match_index == end_index && next_match_index < range.end { + end_index = char_ix_after(next_match_index, text); + match_indices.next(); + } else { + break; + } + } -// let mut match_style = syntax_highlight; -// match_style.weight = Some(FontWeight::BOLD); -// result.push((match_index..end_index, match_style)); -// offset = end_index; -// } + let mut match_style = syntax_highlight; + match_style.font_weight = Some(FontWeight::BOLD); + result.push((match_index..end_index, match_style)); + offset = end_index; + } -// if offset < range.end { -// result.push((offset..range.end, syntax_highlight)); -// } -// } + if offset < range.end { + result.push((offset..range.end, syntax_highlight)); + } + } -// fn char_ix_after(ix: usize, text: &str) -> usize { -// ix + text[ix..].chars().next().unwrap().len_utf8() -// } + fn char_ix_after(ix: usize, text: &str) -> usize { + ix + text[ix..].chars().next().unwrap().len_utf8() + } -// result -// } + result +} // pub fn styled_runs_for_code_label<'a>( // label: &'a CodeLabel, diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 22eb6b3a08..eccf0d6d20 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -50,7 +50,8 @@ fn test_edit_events(cx: &mut TestAppContext) { let editor1 = cx.add_window({ let events = events.clone(); |cx| { - cx.subscribe(cx.view(), move |_, _, event, _| { + let view = cx.view().clone(); + cx.subscribe(&view, move |_, _, event, _| { if matches!(event, Event::Edited | Event::BufferEdited) { events.borrow_mut().push(("editor1", event.clone())); } @@ -63,7 +64,7 @@ fn test_edit_events(cx: &mut TestAppContext) { let editor2 = cx.add_window({ let events = events.clone(); |cx| { - cx.subscribe(cx.view(), move |_, _, event, _| { + cx.subscribe(&cx.view().clone(), move |_, _, event, _| { if matches!(event, Event::Edited | Event::BufferEdited) { events.borrow_mut().push(("editor2", event.clone())); } @@ -156,7 +157,7 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { let mut now = Instant::now(); let buffer = cx.build_model(|cx| language::Buffer::new(0, cx.entity_id().as_u64(), "123456")); - let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); + let group_interval = buffer.update(cx, |buffer, _| buffer.transaction_group_interval()); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx)); @@ -496,10 +497,10 @@ fn test_clone(cx: &mut TestAppContext) { ); assert_set_eq!( cloned_editor - .read_with(cx, |editor, cx| editor.selections.ranges::(cx)) + .update(cx, |editor, cx| editor.selections.ranges::(cx)) .unwrap(), editor - .read_with(cx, |editor, cx| editor.selections.ranges(cx)) + .update(cx, |editor, cx| editor.selections.ranges(cx)) .unwrap() ); assert_set_eq!( @@ -523,7 +524,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { let project = Project::test(fs, [], cx).await; let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); let pane = workspace - .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .update(cx, |workspace, _| workspace.active_pane().clone()) .unwrap(); workspace.update(cx, |v, cx| { @@ -1279,357 +1280,358 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { }); } -#[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; +//todo!(simulate_resize) +// #[gpui::test] +// async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); +// let mut cx = EditorTestContext::new(cx).await; - let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); - let window = cx.window; - window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx); +// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); +// let window = cx.window; +// window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx); - cx.set_state( - &r#"ˇone - two +// cx.set_state( +// &r#"ˇone +// two - three - fourˇ - five +// three +// fourˇ +// five - six"# - .unindent(), - ); +// 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 +// 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(), - ); +// 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 +// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); +// cx.assert_editor_state( +// &r#"one +// two - three - four - five +// three +// four +// five - sixˇ"# - .unindent(), - ); +// sixˇ"# +// .unindent(), +// ); - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); - cx.assert_editor_state( - &r#"one - two +// 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(), - ); +// 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 +// 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(), - ); +// six"# +// .unindent(), +// ); - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); - cx.assert_editor_state( - &r#"ˇone - two +// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); +// cx.assert_editor_state( +// &r#"ˇone +// two - three - four - five +// three +// four +// five - six"# - .unindent(), - ); -} +// six"# +// .unindent(), +// ); +// } -#[gpui::test] -async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); - let window = cx.window; - window.simulate_resize(Point::new(1000., 4. * line_height + 0.5), &mut cx); +// #[gpui::test] +// async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); +// let mut cx = EditorTestContext::new(cx).await; +// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); +// let window = cx.window; +// window.simulate_resize(Point::new(1000., 4. * line_height + 0.5), &mut cx); - cx.set_state( - &r#"ˇone - two - three - four - five - six - seven - eight - nine - ten - "#, - ); +// cx.set_state( +// &r#"ˇone +// two +// three +// four +// five +// six +// seven +// eight +// nine +// ten +// "#, +// ); - cx.update_editor(|editor, cx| { - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 0.) - ); - editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 3.) - ); - editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 6.) - ); - editor.scroll_screen(&ScrollAmount::Page(-1.), cx); - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 3.) - ); +// cx.update_editor(|editor, cx| { +// assert_eq!( +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 0.) +// ); +// editor.scroll_screen(&ScrollAmount::Page(1.), cx); +// assert_eq!( +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 3.) +// ); +// editor.scroll_screen(&ScrollAmount::Page(1.), cx); +// assert_eq!( +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 6.) +// ); +// editor.scroll_screen(&ScrollAmount::Page(-1.), cx); +// assert_eq!( +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 3.) +// ); - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 1.) - ); - editor.scroll_screen(&ScrollAmount::Page(0.5), cx); - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 3.) - ); - }); -} +// editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); +// assert_eq!( +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 1.) +// ); +// editor.scroll_screen(&ScrollAmount::Page(0.5), cx); +// assert_eq!( +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 3.) +// ); +// }); +// } -#[gpui::test] -async fn test_autoscroll(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; +// #[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 line_height = cx.update_editor(|editor, cx| { +// editor.set_vertical_scroll_margin(2, cx); +// editor.style(cx).text.line_height(cx.font_cache()) +// }); - let window = cx.window; - window.simulate_resize(gpui::Point::new(1000., 6.0 * line_height), &mut cx); +// let window = cx.window; +// window.simulate_resize(gpui::Point::new(1000., 6.0 * line_height), &mut cx); - cx.set_state( - &r#"ˇone - two - three - four - five - six - seven - eight - nine - ten - "#, - ); - cx.update_editor(|editor, cx| { - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 0.0) - ); - }); +// cx.set_state( +// &r#"ˇone +// two +// three +// four +// five +// six +// seven +// eight +// nine +// ten +// "#, +// ); +// cx.update_editor(|editor, cx| { +// assert_eq!( +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 0.0) +// ); +// }); - // Add a cursor below the visible area. Since both cursors cannot fit - // on screen, the editor autoscrolls to reveal the newest cursor, and - // allows the vertical scroll margin below that cursor. - cx.update_editor(|editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { - selections.select_ranges([ - Point::new(0, 0)..Point::new(0, 0), - Point::new(6, 0)..Point::new(6, 0), - ]); - }) - }); - cx.update_editor(|editor, cx| { - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 3.0) - ); - }); +// // Add a cursor below the visible area. Since both cursors cannot fit +// // on screen, the editor autoscrolls to reveal the newest cursor, and +// // allows the vertical scroll margin below that cursor. +// cx.update_editor(|editor, cx| { +// editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { +// selections.select_ranges([ +// Point::new(0, 0)..Point::new(0, 0), +// Point::new(6, 0)..Point::new(6, 0), +// ]); +// }) +// }); +// cx.update_editor(|editor, cx| { +// assert_eq!( +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 3.0) +// ); +// }); - // Move down. The editor cursor scrolls down to track the newest cursor. - cx.update_editor(|editor, cx| { - editor.move_down(&Default::default(), cx); - }); - cx.update_editor(|editor, cx| { - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 4.0) - ); - }); +// // Move down. The editor cursor scrolls down to track the newest cursor. +// cx.update_editor(|editor, cx| { +// editor.move_down(&Default::default(), cx); +// }); +// cx.update_editor(|editor, cx| { +// assert_eq!( +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 4.0) +// ); +// }); - // Add a cursor above the visible area. Since both cursors fit on screen, - // the editor scrolls to show both. - cx.update_editor(|editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { - selections.select_ranges([ - Point::new(1, 0)..Point::new(1, 0), - Point::new(6, 0)..Point::new(6, 0), - ]); - }) - }); - cx.update_editor(|editor, cx| { - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 1.0) - ); - }); -} +// // Add a cursor above the visible area. Since both cursors fit on screen, +// // the editor scrolls to show both. +// cx.update_editor(|editor, cx| { +// editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { +// selections.select_ranges([ +// Point::new(1, 0)..Point::new(1, 0), +// Point::new(6, 0)..Point::new(6, 0), +// ]); +// }) +// }); +// cx.update_editor(|editor, cx| { +// assert_eq!( +// editor.snapshot(cx).scroll_position(), +// gpui::Point::new(0., 1.0) +// ); +// }); +// } -#[gpui::test] -async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; +// #[gpui::test] +// async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); +// let mut cx = EditorTestContext::new(cx).await; - let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); - let window = cx.window; - window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx); +// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); +// let window = cx.window; +// window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx); - cx.set_state( - &r#" - ˇone - two - threeˇ - four - five - six - seven - eight - nine - ten - "# - .unindent(), - ); +// cx.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_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(), +// ); - 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(), - ); -} +// // 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) { @@ -3042,7 +3044,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { position: snapshot.anchor_after(Point::new(2, 0)), disposition: BlockDisposition::Below, height: 1, - render: Arc::new(|_| Empty::new().into_any()), + render: Arc::new(|_| div().render()), }], Some(Autoscroll::fit()), cx, @@ -3148,201 +3150,202 @@ fn test_transpose(cx: &mut TestAppContext) { }); } -#[gpui::test] -async fn test_clipboard(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); +//todo!(clipboard) +// #[gpui::test] +// async fn test_clipboard(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; +// 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 "); +// 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 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")), - ); +// // 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"}); +// // 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"}); +// // 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")); +// // 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"}); -} +// // 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, |_| {}); +// #[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)); +// 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(), - ˇ - ); - "}); +// // 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 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(), - ); - "}); +// // 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(), - ˇ); - "}); +// // 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 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 - ) - ˇ - ) - ); - "}); -} +// // 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) { @@ -3785,11 +3788,12 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let view = cx.add_window(|cx| build_editor(buffer, cx)); - view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + + view.condition::(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; - view.update(cx, |view, cx| { + view.update(&mut cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), @@ -3800,8 +3804,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| { view.selections.display_ranges(cx) }) - .unwrap(), + view.update(&mut cx, |view, cx| { view.selections.display_ranges(cx) }), &[ DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), @@ -3809,55 +3812,50 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ] ); - view.update(cx, |view, cx| { + view.update(&mut cx, |view, cx| { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), ] ); - view.update(cx, |view, cx| { + view.update(&mut cx, |view, cx| { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] ); // Trying to expand the selected syntax node one more time has no effect. - view.update(cx, |view, cx| { + view.update(&mut cx, |view, cx| { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] ); - view.update(cx, |view, cx| { + view.update(&mut cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), ] ); - view.update(cx, |view, cx| { + view.update(&mut cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), @@ -3865,12 +3863,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ] ); - view.update(cx, |view, cx| { + view.update(&mut cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), @@ -3879,12 +3876,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ); // Trying to shrink the selected syntax node one more time has no effect. - view.update(cx, |view, cx| { + view.update(&mut cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), @@ -3894,7 +3890,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { // Ensure that we keep expanding the selection if the larger selection starts or ends within // a fold. - view.update(cx, |view, cx| { + view.update(&mut cx, |view, cx| { view.fold_ranges( vec![ Point::new(0, 21)..Point::new(0, 24), @@ -3906,8 +3902,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), + view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), @@ -3959,9 +3954,10 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; editor - .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; editor.update(cx, |editor, cx| { @@ -4524,8 +4520,9 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let view = cx.add_window(|cx| build_editor(buffer, cx)); - view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; + view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; view.update(cx, |view, cx| { @@ -4674,9 +4671,10 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; editor - .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; editor.update(cx, |editor, cx| { @@ -4764,7 +4762,8 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { ); let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; editor.update(cx, |editor, cx| { let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); @@ -4894,7 +4893,8 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); @@ -4916,9 +4916,10 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { .next() .await; cx.executor().start_waiting(); - save.await.unwrap(); + let x = save.await; + assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), + editor.update(cx, |editor, cx| editor.text(cx)), "one, two\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); @@ -4940,9 +4941,9 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); cx.executor().start_waiting(); - save.await.unwrap(); + save.await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), + editor.update(cx, |editor, cx| editor.text(cx)), "one\ntwo\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); @@ -4973,7 +4974,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { .next() .await; cx.executor().start_waiting(); - save.await.unwrap(); + save.await; } #[gpui::test] @@ -5012,7 +5013,8 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); @@ -5034,9 +5036,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { .next() .await; cx.executor().start_waiting(); - save.await.unwrap(); + save.await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), + editor.update(cx, |editor, cx| editor.text(cx)), "one, two\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); @@ -5060,9 +5062,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); cx.executor().start_waiting(); - save.await.unwrap(); + save.await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), + editor.update(cx, |editor, cx| editor.text(cx)), "one\ntwo\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); @@ -5093,7 +5095,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { .next() .await; cx.executor().start_waiting(); - save.await.unwrap(); + save.await; } #[gpui::test] @@ -5139,7 +5141,8 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); let format = editor @@ -5162,9 +5165,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { .next() .await; cx.executor().start_waiting(); - format.await.unwrap(); + format.await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), + editor.update(cx, |editor, cx| editor.text(cx)), "one, two\nthree\n" ); @@ -5185,9 +5188,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); cx.executor().start_waiting(); - format.await.unwrap(); + format.await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(), + editor.update(cx, |editor, cx| editor.text(cx)), "one\ntwo\nthree\n" ); } @@ -5213,7 +5216,7 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { // a newline and an indent before the `.` cx.lsp .handle_request::(move |_, cx| { - let executor = cx.background_executor(); + let executor = cx.background_executor().clone(); async move { executor.timer(Duration::from_millis(100)).await; Ok(Some(vec![lsp::TextEdit { @@ -5366,177 +5369,178 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) ); } -#[gpui::test] -async fn test_completion(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); +//todo!(completion) +// #[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; +// 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 - "}); +// 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 - "}); +// 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.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.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"); +// 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; +// 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 - "}); +// 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(); +// 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(); -} +// 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) { @@ -5925,7 +5929,8 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { multibuffer }); - let view = cx.add_window(|cx| build_editor(multibuffer, cx)); + let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); + let cx = &mut cx; view.update(cx, |view, cx| { assert_eq!(view.text(cx), "aaaa\nbbbb"); view.change_selections(None, cx, |s| { @@ -5995,7 +6000,8 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { multibuffer }); - let view = cx.add_window(|cx| build_editor(multibuffer, cx)); + let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); + let cx = &mut cx; view.update(cx, |view, cx| { let (expected_text, selection_ranges) = marked_text_ranges( indoc! {" @@ -6232,8 +6238,9 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let view = cx.add_window(|cx| build_editor(buffer, cx)); - view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; + view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; view.update(cx, |view, cx| { @@ -6307,7 +6314,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { let mut highlighted_ranges = editor.background_highlights_in_range( anchor_range(Point::new(3, 4)..Point::new(7, 4)), &snapshot, - theme::current(cx).as_ref(), + cx.theme().colors(), ); // Enforce a consistent ordering based on color without relying on the ordering of the // highlight's `TypeId` which is non-executor. @@ -6337,7 +6344,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { editor.background_highlights_in_range( anchor_range(Point::new(5, 6)..Point::new(6, 4)), &snapshot, - theme::current(cx).as_ref(), + cx.theme().colors(), ), &[( DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), @@ -6419,7 +6426,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .unwrap() .await .unwrap(); - follower.read_with(cx, |follower, cx| { + follower.update(cx, |follower, cx| { assert_eq!(follower.selections.ranges(cx), vec![1..1]); }); assert_eq!(*is_still_following.borrow(), true); @@ -6477,7 +6484,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .unwrap() .await .unwrap(); - follower.read_with(cx, |follower, cx| { + follower.update(cx, |follower, cx| { assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); }); assert_eq!(*is_still_following.borrow(), true); @@ -6493,7 +6500,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .unwrap() .await .unwrap(); - follower.read_with(cx, |follower, cx| { + follower.update(cx, |follower, cx| { assert_eq!(follower.selections.ranges(cx), vec![0..2]); }); @@ -6511,170 +6518,171 @@ async fn test_following(cx: &mut gpui::TestAppContext) { assert_eq!(*is_still_following.borrow(), false); } -#[gpui::test] -async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); +//todo!(following) +// #[gpui::test] +// async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let pane = workspace - .read_with(cx, |workspace, _| workspace.active_pane().clone()) - .unwrap(); +// let fs = FakeFs::new(cx.executor()); +// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; +// let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let pane = workspace +// .update(cx, |workspace, _| workspace.active_pane().clone()) +// .unwrap(); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); +// let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - let leader = pane.update(cx, |_, cx| { - let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); - cx.build_view(|cx| build_editor(multibuffer.clone(), cx)) - }); +// let leader = pane.update(cx, |_, cx| { +// let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); +// cx.build_view(|cx| build_editor(multibuffer.clone(), cx)) +// }); - // Start following the editor when it has no excerpts. - 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.root_view(cx).unwrap(), - ViewId { - creator: Default::default(), - id: 0, - }, - &mut state_message, - cx, - ) - }) - .unwrap() - .await - .unwrap(); +// // 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.root_view(cx).unwrap(), +// 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 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(), - ) - }); +// 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, - ); - }); - }); +// // 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.update(cx, |editor, cx| editor.text(cx)), - leader.update(cx, |editor, cx| editor.text(cx)) - ); - update_message.borrow_mut().take(); +// // 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.update(cx, |editor, cx| editor.text(cx)), +// leader.update(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.update(cx, |editor, cx| editor.text(cx)), - leader.update(cx, |editor, cx| editor.text(cx)) - ); +// // 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.update(cx, |editor, cx| editor.text(cx)), +// leader.update(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); - }); - }); +// // 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.update(cx, |editor, cx| editor.text(cx)), - leader.update(cx, |editor, cx| editor.text(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.update(cx, |editor, cx| editor.text(cx)), +// leader.update(cx, |editor, cx| editor.text(cx)) +// ); +// } #[test] fn test_combine_syntax_and_fuzzy_match_highlights() { @@ -7048,255 +7056,256 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { ); } -#[gpui::test(iterations = 10)] -async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); +// todo!(completions) +// #[gpui::test(iterations = 10)] +// async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); - let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| cx.set_global(copilot)); - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; +// 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![], - ); - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { - assert!(editor.context_menu_visible()); - assert!(!editor.has_active_copilot_suggestion(cx)); +// // 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![], +// ); +// executor.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"); - }); +// // 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![], - ); - executor.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"); - }); +// // 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![], +// ); +// executor.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![], - ); - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { - assert!(editor.context_menu_visible()); - assert!(!editor.has_active_copilot_suggestion(cx)); +// // 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![], +// ); +// executor.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"); - }); +// // 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"); - executor.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"); - }); +// // Ensure existing completion is interpolated when inserting again. +// cx.simulate_keystroke("c"); +// executor.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![], - ); - executor.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"); +// // 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![], +// ); +// executor.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"); +// // 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"); +// // 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"); - }); +// // 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"); +// // 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"); +// // 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"); +// // 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"); - }); +// // 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"); - }); +// // 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![], - ); +// // 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)); - executor.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}"); +// cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); +// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// cx.update_editor(|editor, cx| { +// assert!(editor.has_active_copilot_suggestion(cx)); +// assert_eq!(editor.display_text(cx), "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 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}"); - }); -} +// // 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( @@ -7606,7 +7615,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { let worktree_id = workspace .update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { + workspace.project().update(cx, |project, cx| { project.worktrees().next().unwrap().read(cx).id() }) }) @@ -7648,7 +7657,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { }); editor_handle.update(cx, |editor, cx| { - cx.focus(&editor_handle); + editor.focus(cx); editor.change_selections(None, cx, |s| { s.select_ranges([Point::new(0, 21)..Point::new(0, 20)]) }); @@ -7657,7 +7666,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { cx.executor().run_until_parked(); - buffer.read_with(cx, |buffer, _| { + buffer.update(cx, |buffer, _| { assert_eq!( buffer.text(), "fn main() { let a = {5}; }", @@ -7802,196 +7811,197 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test ); } -#[gpui::test] -async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); +//todo!(completions) +// #[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; +// 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() - }; +// 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, - ]))) - } - }); +// 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; +// 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.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)ˇ; }"}); -} +// 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, |_| {}); +// #[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; +// 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.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#"

"#); +// 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.executor().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"); - } - }); +// // 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.executor().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.executor().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"); - } - }); +// cx.simulate_keystroke("l"); +// cx.executor().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.executor().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"); - } - }); -} +// // When filtering completions, consider the character after the '-' to +// // be the start of a subword. +// cx.set_state(r#"

"#); +// cx.simulate_keystroke("l"); +// cx.executor().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) { @@ -8032,17 +8042,18 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { let buffer_text = "one\ntwo\nthree\n"; let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let cx = &mut cx; editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); - let format = editor + editor .update(cx, |editor, cx| { editor.perform_format(project.clone(), FormatTrigger::Manual, cx) }) - .unwrap(); - format.await.unwrap(); + .unwrap() + .await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.update(cx, |editor, cx| editor.text(cx)), buffer_text.to_string() + prettier_format_suffix, "Test prettier formatting was not applied to the original buffer text", ); @@ -8055,7 +8066,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { }); format.await.unwrap(); assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.update(cx, |editor, cx| editor.text(cx)), buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, "Autoformatting (via test prettier) was not applied to the original buffer text", ); @@ -8185,7 +8196,7 @@ pub(crate) fn update_test_language_settings( f: impl Fn(&mut AllLanguageSettingsContent), ) { cx.update(|cx| { - cx.update_global::(|store, cx| { + cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, f); }); }); @@ -8196,7 +8207,7 @@ pub(crate) fn update_test_project_settings( f: impl Fn(&mut ProjectSettings), ) { cx.update(|cx| { - cx.update_global::(|store, cx| { + cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, f); }); }); @@ -8204,7 +8215,8 @@ pub(crate) fn update_test_project_settings( pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) { cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); + let store = SettingsStore::test(cx); + cx.set_global(store); theme::init(cx); client::init_settings(cx); language::init(cx); diff --git a/crates/editor2/src/test/editor_lsp_test_context.rs b/crates/editor2/src/test/editor_lsp_test_context.rs index d48e911a9f..afcefad6b2 100644 --- a/crates/editor2/src/test/editor_lsp_test_context.rs +++ b/crates/editor2/src/test/editor_lsp_test_context.rs @@ -10,7 +10,7 @@ use serde_json::json; use crate::{Editor, ToPoint}; use collections::HashSet; use futures::Future; -use gpui::{json, View, ViewContext}; +use gpui::{View, ViewContext, VisualTestContext}; use indoc::indoc; use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; use lsp::{notification, request}; @@ -19,7 +19,7 @@ use project::Project; use smol::stream::StreamExt; use workspace::{AppState, Workspace, WorkspaceHandle}; -use super::editor_test_context::EditorTestContext; +use super::editor_test_context::{AssertionContextManager, EditorTestContext}; pub struct EditorLspTestContext<'a> { pub cx: EditorTestContext<'a>, @@ -34,8 +34,6 @@ impl<'a> EditorLspTestContext<'a> { capabilities: lsp::ServerCapabilities, cx: &'a mut gpui::TestAppContext, ) -> EditorLspTestContext<'a> { - use json::json; - let app_state = cx.update(AppState::test); cx.update(|cx| { @@ -70,9 +68,10 @@ impl<'a> EditorLspTestContext<'a> { .await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); + let workspace = window.root_view(cx).unwrap(); + let mut cx = VisualTestContext::from_window(*window.deref(), cx); project - .update(cx, |project, cx| { + .update(&mut cx, |project, cx| { project.find_or_create_local_worktree("/root", true, cx) }) .await @@ -82,7 +81,7 @@ impl<'a> EditorLspTestContext<'a> { let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let item = workspace - .update(cx, |workspace, cx| { + .update(&mut cx, |workspace, cx| { workspace.open_path(file, None, true, cx) }) .await @@ -92,7 +91,7 @@ impl<'a> EditorLspTestContext<'a> { item.act_as::(cx) .expect("Opened test file wasn't an editor") }); - editor.update(cx, |_, cx| cx.focus_self()); + editor.update(&mut cx, |editor, cx| editor.focus(cx)); let lsp = fake_servers.next().await.unwrap(); @@ -101,6 +100,7 @@ impl<'a> EditorLspTestContext<'a> { cx, window: window.into(), editor, + assertion_cx: AssertionContextManager::new(), }, lsp, workspace, @@ -258,7 +258,7 @@ impl<'a> EditorLspTestContext<'a> { where F: FnOnce(&mut Workspace, &mut ViewContext) -> T, { - self.workspace.update(self.cx.cx, update) + self.workspace.update(&mut self.cx.cx, update) } pub fn handle_request( diff --git a/crates/editor2/src/test/editor_test_context.rs b/crates/editor2/src/test/editor_test_context.rs index 304e4fcd2b..c865538b0c 100644 --- a/crates/editor2/src/test/editor_test_context.rs +++ b/crates/editor2/src/test/editor_test_context.rs @@ -1,28 +1,37 @@ use crate::{ display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, }; +use collections::BTreeMap; use futures::Future; use gpui::{ AnyWindowHandle, AppContext, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext, + VisualTestContext, WindowHandle, }; use indoc::indoc; +use itertools::Itertools; use language::{Buffer, BufferSnapshot}; +use parking_lot::RwLock; use project::{FakeFs, Project}; use std::{ any::TypeId, ops::{Deref, DerefMut, Range}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, }; use util::{ assert_set_eq, test::{generate_marked_text, marked_text_ranges}, }; -// use super::build_editor_with_project; +use super::build_editor_with_project; pub struct EditorTestContext<'a> { - pub cx: &'a mut gpui::TestAppContext, + pub cx: gpui::VisualTestContext<'a>, pub window: AnyWindowHandle, pub editor: View, + pub assertion_cx: AssertionContextManager, } impl<'a> EditorTestContext<'a> { @@ -43,15 +52,18 @@ impl<'a> EditorTestContext<'a> { }) .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 = cx.add_window(|cx| { + let editor = + build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx); + editor.focus(cx); + editor }); - let editor = window.root(cx); + let editor_view = editor.root_view(cx).unwrap(); Self { - cx, - window: window.into(), - editor, + cx: VisualTestContext::from_window(*editor.deref(), cx), + window: editor.into(), + editor: editor_view, + assertion_cx: AssertionContextManager::new(), } } @@ -59,24 +71,27 @@ impl<'a> EditorTestContext<'a> { &self, predicate: impl FnMut(&Editor, &AppContext) -> bool, ) -> impl Future { - self.editor.condition(self.cx, predicate) + self.editor.condition::(&self.cx, predicate) } - pub fn editor(&self, read: F) -> T + #[track_caller] + pub fn editor(&mut self, read: F) -> T where F: FnOnce(&Editor, &ViewContext) -> T, { - self.editor.update(self.cx, read) + self.editor + .update(&mut self.cx, |this, cx| read(&this, &cx)) } + #[track_caller] pub fn update_editor(&mut self, update: F) -> T where F: FnOnce(&mut Editor, &mut ViewContext) -> T, { - self.editor.update(self.cx, update) + self.editor.update(&mut self.cx, update) } - pub fn multibuffer(&self, read: F) -> T + pub fn multibuffer(&mut self, read: F) -> T where F: FnOnce(&MultiBuffer, &AppContext) -> T, { @@ -90,11 +105,11 @@ impl<'a> EditorTestContext<'a> { self.update_editor(|editor, cx| editor.buffer().update(cx, update)) } - pub fn buffer_text(&self) -> String { + pub fn buffer_text(&mut self) -> String { self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) } - pub fn buffer(&self, read: F) -> T + pub fn buffer(&mut self, read: F) -> T where F: FnOnce(&Buffer, &AppContext) -> T, { @@ -114,10 +129,18 @@ impl<'a> EditorTestContext<'a> { }) } - pub fn buffer_snapshot(&self) -> BufferSnapshot { + pub fn buffer_snapshot(&mut self) -> BufferSnapshot { self.buffer(|buffer, _| buffer.snapshot()) } + pub fn add_assertion_context(&self, context: String) -> ContextHandle { + self.assertion_cx.add_context(context) + } + + pub fn assertion_context(&self) -> String { + self.assertion_cx.context() + } + pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle { let keystroke_under_test_handle = self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); @@ -141,16 +164,12 @@ impl<'a> EditorTestContext<'a> { // 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!(); - } + self.cx.background_executor.run_until_parked(); keystrokes_under_test_handle } - pub fn ranges(&self, marked_text: &str) -> Vec> { + pub fn ranges(&mut self, marked_text: &str) -> Vec> { let (unmarked_text, ranges) = marked_text_ranges(marked_text, false); assert_eq!(self.buffer_text(), unmarked_text); ranges @@ -160,12 +179,12 @@ impl<'a> EditorTestContext<'a> { let ranges = self.ranges(marked_text); let snapshot = self .editor - .update(self.cx, |editor, cx| editor.snapshot(cx)); + .update(&mut self.cx, |editor, cx| editor.snapshot(cx)); ranges[0].start.to_display_point(&snapshot) } // Returns anchors for the current buffer using `«` and `»` - pub fn text_anchor_range(&self, marked_text: &str) -> Range { + pub fn text_anchor_range(&mut self, marked_text: &str) -> Range { let ranges = self.ranges(marked_text); let snapshot = self.buffer_snapshot(); snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) @@ -190,7 +209,7 @@ impl<'a> EditorTestContext<'a> { marked_text.escape_debug().to_string() )); let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); - self.editor.update(self.cx, |editor, cx| { + self.editor.update(&mut self.cx, |editor, cx| { editor.set_text(unmarked_text, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(selection_ranges) @@ -206,7 +225,7 @@ impl<'a> EditorTestContext<'a> { marked_text.escape_debug().to_string() )); let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); - self.editor.update(self.cx, |editor, cx| { + self.editor.update(&mut self.cx, |editor, cx| { assert_eq!(editor.text(cx), unmarked_text); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(selection_ranges) @@ -273,9 +292,12 @@ impl<'a> EditorTestContext<'a> { self.assert_selections(expected_selections, expected_marked_text) } - fn editor_selections(&self) -> Vec> { + #[track_caller] + fn editor_selections(&mut self) -> Vec> { self.editor - .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) + .update(&mut self.cx, |editor, cx| { + editor.selections.all::(cx) + }) .into_iter() .map(|s| { if s.reversed { @@ -320,7 +342,7 @@ impl<'a> Deref for EditorTestContext<'a> { type Target = gpui::TestAppContext; fn deref(&self) -> &Self::Target { - self.cx + &self.cx } } @@ -329,3 +351,50 @@ impl<'a> DerefMut for EditorTestContext<'a> { &mut self.cx } } + +/// Tracks string context to be printed when assertions fail. +/// Often this is done by storing a context string in the manager and returning the handle. +#[derive(Clone)] +pub struct AssertionContextManager { + id: Arc, + contexts: Arc>>, +} + +impl AssertionContextManager { + pub fn new() -> Self { + Self { + id: Arc::new(AtomicUsize::new(0)), + contexts: Arc::new(RwLock::new(BTreeMap::new())), + } + } + + pub fn add_context(&self, context: String) -> ContextHandle { + let id = self.id.fetch_add(1, Ordering::Relaxed); + let mut contexts = self.contexts.write(); + contexts.insert(id, context); + ContextHandle { + id, + manager: self.clone(), + } + } + + pub fn context(&self) -> String { + let contexts = self.contexts.read(); + format!("\n{}\n", contexts.values().join("\n")) + } +} + +/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails. +/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails, +/// the state that was set initially for the failure can be printed in the error message +pub struct ContextHandle { + id: usize, + manager: AssertionContextManager, +} + +impl Drop for ContextHandle { + fn drop(&mut self) { + let mut contexts = self.manager.contexts.write(); + contexts.remove(&self.id); + } +} diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 82c7f11e1c..2e99477674 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -6,7 +6,7 @@ use crate::{ }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; -use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; +use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; #[derive(Clone)] pub struct TestAppContext { @@ -132,6 +132,18 @@ impl TestAppContext { cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)) } + pub fn add_window_view(&mut self, build_window: F) -> (View, VisualTestContext) + where + F: FnOnce(&mut ViewContext) -> V, + V: Render, + { + let mut cx = self.app.borrow_mut(); + let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)); + drop(cx); + let view = window.root_view(self).unwrap(); + (view, VisualTestContext::from_window(*window.deref(), self)) + } + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, @@ -158,7 +170,7 @@ impl TestAppContext { Some(read(lock.try_global()?, &lock)) } - pub fn set_global(&mut self, global: G) { + pub fn set_global(&mut self, global: G) { let mut lock = self.app.borrow_mut(); lock.set_global(global); } @@ -277,6 +289,72 @@ impl Model { } } +impl View { + pub fn condition( + &self, + cx: &TestAppContext, + mut predicate: impl FnMut(&V, &AppContext) -> bool, + ) -> impl Future + where + Evt: 'static, + V: EventEmitter, + { + use postage::prelude::{Sink as _, Stream as _}; + + let (tx, mut rx) = postage::mpsc::channel(1024); + let timeout_duration = Duration::from_millis(100); //todo!() cx.condition_duration(); + + let mut cx = cx.app.borrow_mut(); + let subscriptions = ( + cx.observe(self, { + let mut tx = tx.clone(); + move |_, _| { + tx.blocking_send(()).ok(); + } + }), + cx.subscribe(self, { + let mut tx = tx.clone(); + move |_, _: &Evt, _| { + tx.blocking_send(()).ok(); + } + }), + ); + + let cx = cx.this.upgrade().unwrap(); + let handle = self.downgrade(); + + async move { + crate::util::timeout(timeout_duration, async move { + loop { + { + let cx = cx.borrow(); + let cx = &*cx; + if predicate( + handle + .upgrade() + .expect("view dropped with pending condition") + .read(cx), + cx, + ) { + break; + } + } + + // todo!(start_waiting) + // cx.borrow().foreground_executor().start_waiting(); + rx.recv() + .await + .expect("view dropped with pending condition"); + // cx.borrow().foreground_executor().finish_waiting(); + } + }) + .await + .expect("condition timed out"); + drop(subscriptions); + } + } +} + use derive_more::{Deref, DerefMut}; #[derive(Deref, DerefMut)] pub struct VisualTestContext<'a> { diff --git a/crates/gpui2/src/color.rs b/crates/gpui2/src/color.rs index edf416ae7d..5f6308ec4f 100644 --- a/crates/gpui2/src/color.rs +++ b/crates/gpui2/src/color.rs @@ -167,7 +167,7 @@ impl TryFrom<&'_ str> for Rgba { } } -#[derive(Default, Copy, Clone, Debug, PartialEq, PartialOrd)] +#[derive(Default, Copy, Clone, Debug)] #[repr(C)] pub struct Hsla { pub h: f32, @@ -176,6 +176,35 @@ pub struct Hsla { pub a: f32, } +impl PartialEq for Hsla { + fn eq(&self, other: &Self) -> bool { + self.h + .total_cmp(&other.h) + .then(self.s.total_cmp(&other.s)) + .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) + .is_eq() + } +} + +impl PartialOrd for Hsla { + fn partial_cmp(&self, other: &Self) -> Option { + // SAFETY: The total ordering relies on this always being Some() + Some( + self.h + .total_cmp(&other.h) + .then(self.s.total_cmp(&other.s)) + .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))), + ) + } +} + +impl Ord for Hsla { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // SAFETY: The partial comparison is a total comparison + unsafe { self.partial_cmp(other).unwrap_unchecked() } + } +} + impl Hsla { pub fn to_rgb(self) -> Rgba { self.into() diff --git a/crates/gpui2/src/util.rs b/crates/gpui2/src/util.rs index 1000800881..3ee80306ea 100644 --- a/crates/gpui2/src/util.rs +++ b/crates/gpui2/src/util.rs @@ -1,16 +1,20 @@ +use std::time::Duration; + +use futures::Future; +use smol::future::FutureExt; pub use util::*; -// pub async fn timeout(timeout: Duration, f: F) -> Result -// where -// F: Future, -// { -// let timer = async { -// smol::Timer::after(timeout).await; -// Err(()) -// }; -// let future = async move { Ok(f.await) }; -// timer.race(future).await -// } +pub async fn timeout(timeout: Duration, f: F) -> Result +where + F: Future, +{ + let timer = async { + smol::Timer::after(timeout).await; + Err(()) + }; + let future = async move { Ok(f.await) }; + timer.race(future).await +} #[cfg(any(test, feature = "test-support"))] pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace); diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index e4fc5d35c6..053b9d68af 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -4200,24 +4200,24 @@ impl ViewId { } } -// pub trait WorkspaceHandle { -// fn file_project_paths(&self, cx: &AppContext) -> Vec; -// } +pub trait WorkspaceHandle { + fn file_project_paths(&self, cx: &AppContext) -> Vec; +} -// impl WorkspaceHandle for View { -// fn file_project_paths(&self, cx: &AppContext) -> Vec { -// self.read(cx) -// .worktrees(cx) -// .flat_map(|worktree| { -// let worktree_id = worktree.read(cx).id(); -// worktree.read(cx).files(true, 0).map(move |f| ProjectPath { -// worktree_id, -// path: f.path.clone(), -// }) -// }) -// .collect::>() -// } -// } +impl WorkspaceHandle for View { + fn file_project_paths(&self, cx: &AppContext) -> Vec { + self.read(cx) + .worktrees(cx) + .flat_map(|worktree| { + let worktree_id = worktree.read(cx).id(); + worktree.read(cx).files(true, 0).map(move |f| ProjectPath { + worktree_id, + path: f.path.clone(), + }) + }) + .collect::>() + } +} // impl std::fmt::Debug for OpenPaths { // fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { From 916df3c614aeecd976b01bda429f6a52f44e6ff1 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 15:28:20 -0500 Subject: [PATCH 040/126] Add color converter util --- crates/theme2/util/hex_to_hsla.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 crates/theme2/util/hex_to_hsla.py diff --git a/crates/theme2/util/hex_to_hsla.py b/crates/theme2/util/hex_to_hsla.py new file mode 100644 index 0000000000..f741b93365 --- /dev/null +++ b/crates/theme2/util/hex_to_hsla.py @@ -0,0 +1,20 @@ +import colorsys +import sys + +def hex_to_rgb(hex): + hex = hex.lstrip('#') + return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4)) + +def rgb_to_hsla(rgb): + h, l, s = colorsys.rgb_to_hls(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0) + return (round(h * 360, 1), round(s * 100, 1), round(l * 100, 1), 1.0) + +def hex_to_hsla(hex): + return rgb_to_hsla(hex_to_rgb(hex)) + +if len(sys.argv) != 2: + print("Usage: python util/hex_to_hsla.py ") +else: + hex_color = sys.argv[1] + h, s, l, a = hex_to_hsla(hex_color) + print(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})") From e0547d9acd2482c8d18445cf977a1665dc9953ec Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 15:47:57 -0500 Subject: [PATCH 041/126] Allow arrays of colors to be passed in --- crates/theme2/util/hex_to_hsla.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/crates/theme2/util/hex_to_hsla.py b/crates/theme2/util/hex_to_hsla.py index f741b93365..17faa186d8 100644 --- a/crates/theme2/util/hex_to_hsla.py +++ b/crates/theme2/util/hex_to_hsla.py @@ -3,18 +3,33 @@ import sys def hex_to_rgb(hex): hex = hex.lstrip('#') - return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4)) + if len(hex) == 8: # 8 digit hex color + r, g, b, a = (int(hex[i:i+2], 16) for i in (0, 2, 4, 6)) + return r, g, b, a / 255.0 + else: # 6 digit hex color + return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4)) + (1.0,) def rgb_to_hsla(rgb): h, l, s = colorsys.rgb_to_hls(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0) - return (round(h * 360, 1), round(s * 100, 1), round(l * 100, 1), 1.0) + a = rgb[3] # alpha value + return (round(h * 360, 1), round(s * 100, 1), round(l * 100, 1), round(a, 3)) def hex_to_hsla(hex): return rgb_to_hsla(hex_to_rgb(hex)) if len(sys.argv) != 2: - print("Usage: python util/hex_to_hsla.py ") + print("Usage: python util/hex_to_hsla.py <6 or 8 digit hex color or comma-separated list of colors>") else: - hex_color = sys.argv[1] - h, s, l, a = hex_to_hsla(hex_color) - print(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})") + input_arg = sys.argv[1] + if ',' in input_arg: # comma-separated list of colors + hex_colors = input_arg.split(',') + hslas = [] # output array + for hex_color in hex_colors: + hex_color = hex_color.strip("'\" ") + h, s, l, a = hex_to_hsla(hex_color) + hslas.append(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})") + print(hslas) + else: # single color + hex_color = input_arg.strip("'\"") + h, s, l, a = hex_to_hsla(hex_color) + print(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})") From c1887747b7b09a76d8b0c43ff804681ab1b57d29 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 15:48:05 -0500 Subject: [PATCH 042/126] Add one_dark theme --- crates/theme2/src/default_theme.rs | 84 +++++++++++----------- crates/theme2/src/one_themes.rs | 107 +++++++++++++++++++++++++++++ crates/theme2/src/registry.rs | 6 +- crates/theme2/src/theme2.rs | 1 + 4 files changed, 154 insertions(+), 44 deletions(-) create mode 100644 crates/theme2/src/one_themes.rs diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 95a95c687f..0e15c1a9af 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,58 +1,60 @@ use std::sync::Arc; use crate::{ - default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, - ThemeColors, ThemeFamily, ThemeStyles, + default_color_scales, + one_themes::{one_dark, one_family}, + Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors, + ThemeFamily, ThemeStyles, }; -fn zed_pro_daylight() -> Theme { - Theme { - id: "zed_pro_daylight".to_string(), - name: "Zed Pro Daylight".into(), - appearance: Appearance::Light, - styles: ThemeStyles { - system: SystemColors::default(), - colors: ThemeColors::light(), - status: StatusColors::light(), - player: PlayerColors::light(), - syntax: Arc::new(SyntaxTheme::light()), - }, - } -} +// fn zed_pro_daylight() -> Theme { +// Theme { +// id: "zed_pro_daylight".to_string(), +// name: "Zed Pro Daylight".into(), +// appearance: Appearance::Light, +// styles: ThemeStyles { +// system: SystemColors::default(), +// colors: ThemeColors::light(), +// status: StatusColors::light(), +// player: PlayerColors::light(), +// syntax: Arc::new(SyntaxTheme::light()), +// }, +// } +// } -pub(crate) fn zed_pro_moonlight() -> Theme { - Theme { - id: "zed_pro_moonlight".to_string(), - name: "Zed Pro Moonlight".into(), - appearance: Appearance::Dark, - styles: ThemeStyles { - system: SystemColors::default(), - colors: ThemeColors::dark(), - status: StatusColors::dark(), - player: PlayerColors::dark(), - syntax: Arc::new(SyntaxTheme::dark()), - }, - } -} +// pub(crate) fn zed_pro_moonlight() -> Theme { +// Theme { +// id: "zed_pro_moonlight".to_string(), +// name: "Zed Pro Moonlight".into(), +// appearance: Appearance::Dark, +// styles: ThemeStyles { +// system: SystemColors::default(), +// colors: ThemeColors::dark(), +// status: StatusColors::dark(), +// player: PlayerColors::dark(), +// syntax: Arc::new(SyntaxTheme::dark()), +// }, +// } +// } -pub fn zed_pro_family() -> ThemeFamily { - ThemeFamily { - id: "zed_pro".to_string(), - name: "Zed Pro".into(), - author: "Zed Team".into(), - themes: vec![zed_pro_daylight(), zed_pro_moonlight()], - scales: default_color_scales(), - } -} +// pub fn zed_pro_family() -> ThemeFamily { +// ThemeFamily { +// id: "zed_pro".to_string(), +// name: "Zed Pro".into(), +// author: "Zed Team".into(), +// themes: vec![zed_pro_daylight(), zed_pro_moonlight()], +// scales: default_color_scales(), +// } +// } impl Default for ThemeFamily { fn default() -> Self { - zed_pro_family() + one_family() } } impl Default for Theme { fn default() -> Self { - zed_pro_daylight() + one_dark() } } diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs new file mode 100644 index 0000000000..e7d6d383ac --- /dev/null +++ b/crates/theme2/src/one_themes.rs @@ -0,0 +1,107 @@ +use std::sync::Arc; + +use gpui::{hsla, rgba}; + +use crate::{ + black, blue, cyan, default_color_scales, green, neutral, red, violet, yellow, Appearance, + PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeFamily, + ThemeStyles, +}; + +pub fn one_family() -> ThemeFamily { + ThemeFamily { + id: "one".to_string(), + name: "One".into(), + author: "".into(), + themes: vec![one_dark()], + scales: default_color_scales(), + } +} + +pub(crate) fn one_dark() -> Theme { + // let bg = rgba(0x22252A).into(); + // let editor = rgba(0x292C33).into(); + + let bg = hsla(218. / 360., 11. / 100., 15. / 100., 1.); + let editor = hsla(222. / 360., 11. / 100., 18. / 100., 1.); + + Theme { + id: "one_dark".to_string(), + name: "One Dark".into(), + appearance: Appearance::Dark, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors { + border: hsla(225. / 360., 13. / 100., 12. / 100., 1.), + border_variant: hsla(228. / 360., 8. / 100., 25. / 100., 1.), + border_focused: hsla(223. / 360., 78. / 100., 65. / 100., 1.), + border_selected: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), + border_transparent: SystemColors::default().transparent, + border_disabled: hsla(222.0 / 360., 11.6 / 100., 33.7 / 100., 1.0), + elevated_surface_background: bg, + surface_background: bg, + background: bg, + element_background: hsla(222.9 / 360., 11.1 / 100., 24.7 / 100., 1.0), + element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), + element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), + element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0), + ghost_element_background: SystemColors::default().transparent, + ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), + ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), + ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + ghost_element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + text: hsla(222.9 / 360., 9.1 / 100., 84.9 / 100., 1.0), + text_muted: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), + text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), + text_disabled: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), + text_accent: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), + icon: hsla(222.9 / 360., 9.9 / 100., 86.1 / 100., 1.0), + icon_muted: hsla(220.0 / 360., 12.1 / 100., 66.1 / 100., 1.0), + icon_disabled: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), + icon_placeholder: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), + icon_accent: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), + status_bar_background: bg, + title_bar_background: bg, + toolbar_background: editor, + tab_bar_background: bg, + tab_inactive_background: bg, + tab_active_background: editor, + editor_background: editor, + editor_gutter_background: editor, + editor_subheader_background: bg, + editor_active_line_background: hsla(222.9 / 360., 13.5 / 100., 20.4 / 100., 1.0), + editor_highlighted_line_background: gpui::red(), + editor_line_number: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), + editor_active_line_number: hsla(216.0 / 360., 5.9 / 100., 49.6 / 100., 1.0), + editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), + editor_wrap_guide: gpui::red(), + editor_active_wrap_guide: gpui::red(), + editor_document_highlight_read_background: gpui::red(), + editor_document_highlight_write_background: gpui::red(), + terminal_background: bg, + // todo!("Use one colors for terminal") + terminal_ansi_black: black().dark().step_12(), + terminal_ansi_red: red().dark().step_11(), + terminal_ansi_green: green().dark().step_11(), + terminal_ansi_yellow: yellow().dark().step_11(), + terminal_ansi_blue: blue().dark().step_11(), + terminal_ansi_magenta: violet().dark().step_11(), + terminal_ansi_cyan: cyan().dark().step_11(), + terminal_ansi_white: neutral().dark().step_12(), + terminal_ansi_bright_black: black().dark().step_11(), + terminal_ansi_bright_red: red().dark().step_10(), + terminal_ansi_bright_green: green().dark().step_10(), + terminal_ansi_bright_yellow: yellow().dark().step_10(), + terminal_ansi_bright_blue: blue().dark().step_10(), + terminal_ansi_bright_magenta: violet().dark().step_10(), + terminal_ansi_bright_cyan: cyan().dark().step_10(), + terminal_ansi_bright_white: neutral().dark().step_11(), + }, + status: StatusColors::dark(), + player: PlayerColors::dark(), + syntax: Arc::new(SyntaxTheme::dark()), + }, + } +} diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index c8773ea08b..ddf1225e51 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -6,8 +6,8 @@ use gpui::{HighlightStyle, SharedString}; use refineable::Refineable; use crate::{ - zed_pro_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, - ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, + one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, + Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, }; pub struct ThemeRegistry { @@ -105,7 +105,7 @@ impl Default for ThemeRegistry { themes: HashMap::default(), }; - this.insert_theme_families([zed_pro_family()]); + this.insert_theme_families([one_family()]); #[cfg(not(feature = "importing-themes"))] this.insert_user_theme_familes(crate::all_user_themes()); diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index 06d132d39d..b6790b5a6f 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -1,5 +1,6 @@ mod default_colors; mod default_theme; +mod one_themes; mod registry; mod scale; mod settings; From f8bc9be2841d9f5e54f73de48fe1682ecddf6de9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 13:52:49 -0700 Subject: [PATCH 043/126] Fix test --- crates/gpui2/src/keymap/context.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/gpui2/src/keymap/context.rs b/crates/gpui2/src/keymap/context.rs index 99a95531a2..b9cb0384ec 100644 --- a/crates/gpui2/src/keymap/context.rs +++ b/crates/gpui2/src/keymap/context.rs @@ -312,15 +312,15 @@ mod tests { #[test] fn test_parse_context() { let mut expected = KeyContext::default(); - expected.set("foo", "bar"); expected.add("baz"); + expected.set("foo", "bar"); assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected); - assert_eq!(KeyContext::parse("foo = bar baz").unwrap(), expected); + assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected); assert_eq!( KeyContext::parse(" baz foo = bar baz").unwrap(), expected ); - assert_eq!(KeyContext::parse(" foo = bar baz").unwrap(), expected); + assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected); } #[test] From 97d6e7f2f533f65884f25a252c8e193ea8174b48 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 17:09:55 -0500 Subject: [PATCH 044/126] Add one syntax theme --- crates/theme2/src/one_themes.rs | 150 +++++++++++++++++++++++++------- 1 file changed, 120 insertions(+), 30 deletions(-) diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index e7d6d383ac..b7987391e4 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -1,11 +1,10 @@ use std::sync::Arc; -use gpui::{hsla, rgba}; +use gpui::{hsla, FontStyle, FontWeight, HighlightStyle}; use crate::{ - black, blue, cyan, default_color_scales, green, neutral, red, violet, yellow, Appearance, - PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeFamily, - ThemeStyles, + default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, + ThemeColors, ThemeFamily, ThemeStyles, }; pub fn one_family() -> ThemeFamily { @@ -19,11 +18,17 @@ pub fn one_family() -> ThemeFamily { } pub(crate) fn one_dark() -> Theme { - // let bg = rgba(0x22252A).into(); - // let editor = rgba(0x292C33).into(); + let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.); + let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.); - let bg = hsla(218. / 360., 11. / 100., 15. / 100., 1.); - let editor = hsla(222. / 360., 11. / 100., 18. / 100., 1.); + let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0); + let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0); + let green = hsla(95. / 360., 38. / 100., 62. / 100., 1.0); + let orange = hsla(29. / 360., 54. / 100., 61. / 100., 1.0); + let purple = hsla(286. / 360., 51. / 100., 64. / 100., 1.0); + let red = hsla(355. / 360., 65. / 100., 65. / 100., 1.0); + let teal = hsla(187. / 360., 47. / 100., 55. / 100., 1.0); + let yellow = hsla(39. / 360., 67. / 100., 69. / 100., 1.0); Theme { id: "one_dark".to_string(), @@ -61,7 +66,7 @@ pub(crate) fn one_dark() -> Theme { icon_muted: hsla(220.0 / 360., 12.1 / 100., 66.1 / 100., 1.0), icon_disabled: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), icon_placeholder: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), - icon_accent: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), + icon_accent: blue.into(), status_bar_background: bg, title_bar_background: bg, toolbar_background: editor, @@ -72,36 +77,121 @@ pub(crate) fn one_dark() -> Theme { editor_gutter_background: editor, editor_subheader_background: bg, editor_active_line_background: hsla(222.9 / 360., 13.5 / 100., 20.4 / 100., 1.0), - editor_highlighted_line_background: gpui::red(), + editor_highlighted_line_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.1), editor_line_number: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), editor_active_line_number: hsla(216.0 / 360., 5.9 / 100., 49.6 / 100., 1.0), editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), - editor_wrap_guide: gpui::red(), + editor_wrap_guide: gpui::black(), editor_active_wrap_guide: gpui::red(), - editor_document_highlight_read_background: gpui::red(), + editor_document_highlight_read_background: hsla( + 207.8 / 360., + 81. / 100., + 66. / 100., + 0.2, + ), editor_document_highlight_write_background: gpui::red(), terminal_background: bg, // todo!("Use one colors for terminal") - terminal_ansi_black: black().dark().step_12(), - terminal_ansi_red: red().dark().step_11(), - terminal_ansi_green: green().dark().step_11(), - terminal_ansi_yellow: yellow().dark().step_11(), - terminal_ansi_blue: blue().dark().step_11(), - terminal_ansi_magenta: violet().dark().step_11(), - terminal_ansi_cyan: cyan().dark().step_11(), - terminal_ansi_white: neutral().dark().step_12(), - terminal_ansi_bright_black: black().dark().step_11(), - terminal_ansi_bright_red: red().dark().step_10(), - terminal_ansi_bright_green: green().dark().step_10(), - terminal_ansi_bright_yellow: yellow().dark().step_10(), - terminal_ansi_bright_blue: blue().dark().step_10(), - terminal_ansi_bright_magenta: violet().dark().step_10(), - terminal_ansi_bright_cyan: cyan().dark().step_10(), - terminal_ansi_bright_white: neutral().dark().step_11(), + terminal_ansi_black: crate::black().dark().step_12(), + terminal_ansi_red: crate::red().dark().step_11(), + terminal_ansi_green: crate::green().dark().step_11(), + terminal_ansi_yellow: crate::yellow().dark().step_11(), + terminal_ansi_blue: crate::blue().dark().step_11(), + terminal_ansi_magenta: crate::violet().dark().step_11(), + terminal_ansi_cyan: crate::cyan().dark().step_11(), + terminal_ansi_white: crate::neutral().dark().step_12(), + terminal_ansi_bright_black: crate::black().dark().step_11(), + terminal_ansi_bright_red: crate::red().dark().step_10(), + terminal_ansi_bright_green: crate::green().dark().step_10(), + terminal_ansi_bright_yellow: crate::yellow().dark().step_10(), + terminal_ansi_bright_blue: crate::blue().dark().step_10(), + terminal_ansi_bright_magenta: crate::violet().dark().step_10(), + terminal_ansi_bright_cyan: crate::cyan().dark().step_10(), + terminal_ansi_bright_white: crate::neutral().dark().step_11(), + }, + status: StatusColors { + conflict: yellow, + created: green, + deleted: red, + error: red, + hidden: gray, + hint: blue, + ignored: gray, + info: blue, + modified: yellow, + predictive: gray, + renamed: blue, + success: green, + unreachable: gray, + warning: yellow, }, - status: StatusColors::dark(), player: PlayerColors::dark(), - syntax: Arc::new(SyntaxTheme::dark()), + syntax: Arc::new(SyntaxTheme { + highlights: vec![ + ("attribute".into(), purple.into()), + ("boolean".into(), orange.into()), + ("comment".into(), gray.into()), + ("comment.doc".into(), gray.into()), + ("constant".into(), yellow.into()), + ("constructor".into(), blue.into()), + ("embedded".into(), HighlightStyle::default()), + ( + "emphasis".into(), + HighlightStyle { + font_style: Some(FontStyle::Italic), + ..HighlightStyle::default() + }, + ), + ( + "emphasis.strong".into(), + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..HighlightStyle::default() + }, + ), + ("enum".into(), HighlightStyle::default()), + ("function".into(), blue.into()), + ("function.method".into(), blue.into()), + ("function.definition".into(), blue.into()), + ("hint".into(), blue.into()), + ("keyword".into(), purple.into()), + ("label".into(), HighlightStyle::default()), + ("link_text".into(), blue.into()), + ( + "link_uri".into(), + HighlightStyle { + color: Some(teal.into()), + font_style: Some(FontStyle::Italic), + ..HighlightStyle::default() + }, + ), + ("number".into(), orange.into()), + ("operator".into(), HighlightStyle::default()), + ("predictive".into(), HighlightStyle::default()), + ("preproc".into(), HighlightStyle::default()), + ("primary".into(), HighlightStyle::default()), + ("property".into(), red.into()), + ("punctuation".into(), HighlightStyle::default()), + ("punctuation.bracket".into(), HighlightStyle::default()), + ("punctuation.delimiter".into(), HighlightStyle::default()), + ("punctuation.list_marker".into(), HighlightStyle::default()), + ("punctuation.special".into(), HighlightStyle::default()), + ("string".into(), green.into()), + ("string.escape".into(), HighlightStyle::default()), + ("string.regex".into(), red.into()), + ("string.special".into(), HighlightStyle::default()), + ("string.special.symbol".into(), HighlightStyle::default()), + ("tag".into(), HighlightStyle::default()), + ("text.literal".into(), HighlightStyle::default()), + ("title".into(), HighlightStyle::default()), + ("type".into(), teal.into()), + ("variable".into(), HighlightStyle::default()), + ("variable.special".into(), red.into()), + ("variant".into(), HighlightStyle::default()), + ], + inlay_style: HighlightStyle::default(), + suggestion_style: HighlightStyle::default(), + }), }, } } From dd434588eef4f0cebd5fcfc4179e00c2f435debc Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 17:38:44 -0500 Subject: [PATCH 045/126] WIP --- crates/editor2/src/items.rs | 14 +++++++++----- crates/theme2/src/one_themes.rs | 5 +++-- crates/ui2/src/components/tooltip.rs | 15 +++++++-------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 25e9f91608..9614082ccf 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -30,6 +30,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; +use ui::{Label, LabelColor}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::{ @@ -595,16 +596,19 @@ impl Item for Editor { .flex_row() .items_center() .gap_2() - .child(self.title(cx).to_string()) + .child(Label::new(self.title(cx).to_string())) .children(detail.and_then(|detail| { let path = path_for_buffer(&self.buffer, detail, false, cx)?; let description = path.to_string_lossy(); Some( - div() - .text_color(theme.colors().text_muted) - .text_xs() - .child(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)), + div().child( + Label::new(util::truncate_and_trailoff( + &description, + MAX_TAB_TITLE_LEN, + )) + .color(LabelColor::Muted), + ), ) })), ) diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index b7987391e4..6e32eace73 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -20,6 +20,7 @@ pub fn one_family() -> ThemeFamily { pub(crate) fn one_dark() -> Theme { let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.); let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.); + let elevated_surface = hsla(220. / 360., 12. / 100., 18. / 100., 1.); let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0); let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0); @@ -43,10 +44,10 @@ pub(crate) fn one_dark() -> Theme { border_selected: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), border_transparent: SystemColors::default().transparent, border_disabled: hsla(222.0 / 360., 11.6 / 100., 33.7 / 100., 1.0), - elevated_surface_background: bg, + elevated_surface_background: elevated_surface, surface_background: bg, background: bg, - element_background: hsla(222.9 / 360., 11.1 / 100., 24.7 / 100., 1.0), + element_background: elevated_surface, element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 87860ce943..e6c0e3f44d 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,6 +1,8 @@ use gpui::{div, Div, ParentElement, Render, SharedString, Styled, ViewContext}; use theme2::ActiveTheme; +use crate::StyledExt; + #[derive(Clone, Debug)] pub struct TextTooltip { title: SharedString, @@ -16,16 +18,13 @@ impl Render for TextTooltip { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let theme = cx.theme(); div() - .bg(theme.colors().background) - .rounded_lg() - .border() + .elevation_2(cx) .font("Zed Sans") - .border_color(theme.colors().border) - .text_color(theme.colors().text) - .pl_2() - .pr_2() + .text_ui() + .text_color(cx.theme().colors().text) + .py_1() + .px_2() .child(self.title.clone()) } } From 25bc8988072bfc1430c954ffd4131782b5896f3a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 15:33:22 -0700 Subject: [PATCH 046/126] Add KeyBindings to CommandPalette --- .../command_palette2/src/command_palette.rs | 70 ++------ crates/gpui2/src/key_dispatch.rs | 13 +- crates/gpui2/src/keymap/binding.rs | 16 +- crates/gpui2/src/window.rs | 21 ++- crates/ui2/src/components/keybinding.rs | 157 +++++------------- crates/ui2/src/components/palette.rs | 54 ++---- crates/ui2/src/static_data.rs | 58 ++----- 7 files changed, 116 insertions(+), 273 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index c7a6c9ee83..e403a50cf9 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -11,7 +11,7 @@ use std::{ sync::Arc, }; use theme::ActiveTheme; -use ui::{v_stack, HighlightedLabel, StyledExt}; +use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -318,66 +318,16 @@ impl PickerDelegate for CommandPaletteDelegate { .rounded_md() .when(selected, |this| this.bg(colors.ghost_element_selected)) .hover(|this| this.bg(colors.ghost_element_hover)) - .child(HighlightedLabel::new( - command.name.clone(), - r#match.positions.clone(), - )) + .child( + h_stack() + .justify_between() + .child(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) + .children(KeyBinding::for_action(&*command.action, cx)), + ) } - - // fn render_match( - // &self, - // ix: usize, - // mouse_state: &mut MouseState, - // selected: bool, - // cx: &gpui::AppContext, - // ) -> AnyElement> { - // let mat = &self.matches[ix]; - // let command = &self.actions[mat.candidate_id]; - // let theme = theme::current(cx); - // let style = theme.picker.item.in_state(selected).style_for(mouse_state); - // let key_style = &theme.command_palette.key.in_state(selected); - // let keystroke_spacing = theme.command_palette.keystroke_spacing; - - // Flex::row() - // .with_child( - // Label::new(mat.string.clone(), style.label.clone()) - // .with_highlights(mat.positions.clone()), - // ) - // .with_children(command.keystrokes.iter().map(|keystroke| { - // Flex::row() - // .with_children( - // [ - // (keystroke.ctrl, "^"), - // (keystroke.alt, "⌥"), - // (keystroke.cmd, "⌘"), - // (keystroke.shift, "⇧"), - // ] - // .into_iter() - // .filter_map(|(modifier, label)| { - // if modifier { - // Some( - // Label::new(label, key_style.label.clone()) - // .contained() - // .with_style(key_style.container), - // ) - // } else { - // None - // } - // }), - // ) - // .with_child( - // Label::new(keystroke.key.clone(), key_style.label.clone()) - // .contained() - // .with_style(key_style.container), - // ) - // .contained() - // .with_margin_left(keystroke_spacing) - // .flex_float() - // })) - // .contained() - // .with_style(style.container) - // .into_any() - // } } fn humanize_action_name(name: &str) -> String { diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 8ace4188ae..323fd7d2ff 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -1,7 +1,7 @@ use crate::{ build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, - FocusId, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, Pixels, - Style, StyleRefinement, ViewContext, WindowContext, + FocusId, KeyBinding, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, + Pixels, Style, StyleRefinement, ViewContext, WindowContext, }; use collections::HashMap; use parking_lot::Mutex; @@ -145,6 +145,15 @@ impl DispatchTree { actions } + pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + self.keymap + .lock() + .bindings_for_action(action.type_id()) + .filter(|candidate| candidate.action.partial_eq(action)) + .cloned() + .collect() + } + pub fn dispatch_key( &mut self, keystroke: &Keystroke, diff --git a/crates/gpui2/src/keymap/binding.rs b/crates/gpui2/src/keymap/binding.rs index 9fbd0018b9..e55d664610 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -3,9 +3,19 @@ use anyhow::Result; use smallvec::SmallVec; pub struct KeyBinding { - action: Box, - pub(super) keystrokes: SmallVec<[Keystroke; 2]>, - pub(super) context_predicate: Option, + pub(crate) action: Box, + pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, + pub(crate) context_predicate: Option, +} + +impl Clone for KeyBinding { + fn clone(&self) -> Self { + KeyBinding { + action: self.action.boxed_clone(), + keystrokes: self.keystrokes.clone(), + context_predicate: self.context_predicate.clone(), + } + } } impl KeyBinding { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 4a7241a5c5..9eab178152 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -3,13 +3,13 @@ use crate::{ AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, - InputEvent, IsZero, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, Modifiers, - MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, - PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, - PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, - SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, - TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, - WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, + Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, + PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, + RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, + Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, + WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -1377,6 +1377,13 @@ impl<'a> WindowContext<'a> { Vec::new() } } + + pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + self.window + .current_frame + .dispatch_tree + .bindings_for_action(action) + } } impl Context for WindowContext<'_> { diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index bd02e694ed..b6c435c607 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -1,50 +1,42 @@ -use std::collections::HashSet; - -use strum::{EnumIter, IntoEnumIterator}; +use gpui::Action; +use strum::EnumIter; use crate::prelude::*; #[derive(Component)] -pub struct Keybinding { +pub struct KeyBinding { /// A keybinding consists of a key and a set of modifier keys. /// More then one keybinding produces a chord. /// /// This should always contain at least one element. - keybinding: Vec<(String, ModifierKeys)>, + key_binding: gpui::KeyBinding, } -impl Keybinding { - pub fn new(key: String, modifiers: ModifierKeys) -> Self { - Self { - keybinding: vec![(key, modifiers)], - } +impl KeyBinding { + pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option { + // todo! this last is arbitrary, we want to prefer users key bindings over defaults, + // and vim over normal (in vim mode), etc. + let key_binding = cx.bindings_for_action(action).last().cloned()?; + Some(Self::new(key_binding)) } - pub fn new_chord( - first_note: (String, ModifierKeys), - second_note: (String, ModifierKeys), - ) -> Self { - Self { - keybinding: vec![first_note, second_note], - } + pub fn new(key_binding: gpui::KeyBinding) -> Self { + Self { key_binding } } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { div() .flex() .gap_2() - .children(self.keybinding.iter().map(|(key, modifiers)| { + .children(self.key_binding.keystrokes().iter().map(|keystroke| { div() .flex() .gap_1() - .children(ModifierKey::iter().filter_map(|modifier| { - if modifiers.0.contains(&modifier) { - Some(Key::new(modifier.glyph().to_string())) - } else { - None - } - })) - .child(Key::new(key.clone())) + .when(keystroke.modifiers.control, |el| el.child(Key::new("^"))) + .when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥"))) + .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘"))) + .when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧"))) + .child(Key::new(keystroke.key.clone())) })) } } @@ -81,76 +73,6 @@ pub enum ModifierKey { Shift, } -impl ModifierKey { - /// Returns the glyph for the [`ModifierKey`]. - pub fn glyph(&self) -> char { - match self { - Self::Control => '^', - Self::Alt => '⌥', - Self::Command => '⌘', - Self::Shift => '⇧', - } - } -} - -#[derive(Clone)] -pub struct ModifierKeys(HashSet); - -impl ModifierKeys { - pub fn new() -> Self { - Self(HashSet::new()) - } - - pub fn all() -> Self { - Self(HashSet::from_iter(ModifierKey::iter())) - } - - pub fn add(mut self, modifier: ModifierKey) -> Self { - self.0.insert(modifier); - self - } - - pub fn control(mut self, control: bool) -> Self { - if control { - self.0.insert(ModifierKey::Control); - } else { - self.0.remove(&ModifierKey::Control); - } - - self - } - - pub fn alt(mut self, alt: bool) -> Self { - if alt { - self.0.insert(ModifierKey::Alt); - } else { - self.0.remove(&ModifierKey::Alt); - } - - self - } - - pub fn command(mut self, command: bool) -> Self { - if command { - self.0.insert(ModifierKey::Command); - } else { - self.0.remove(&ModifierKey::Command); - } - - self - } - - pub fn shift(mut self, shift: bool) -> Self { - if shift { - self.0.insert(ModifierKey::Shift); - } else { - self.0.remove(&ModifierKey::Shift); - } - - self - } -} - #[cfg(feature = "stories")] pub use stories::*; @@ -158,29 +80,38 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{action, Div, Render}; use itertools::Itertools; pub struct KeybindingStory; + #[action] + struct NoAction {} + + pub fn binding(key: &str) -> gpui::KeyBinding { + gpui::KeyBinding::new(key, NoAction {}, None) + } + impl Render for KeybindingStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let all_modifier_permutations = ModifierKey::iter().permutations(2); + let all_modifier_permutations = + ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2); Story::container(cx) - .child(Story::title_for::<_, Keybinding>(cx)) + .child(Story::title_for::<_, KeyBinding>(cx)) .child(Story::label(cx, "Single Key")) - .child(Keybinding::new("Z".to_string(), ModifierKeys::new())) + .child(KeyBinding::new(binding("Z"))) .child(Story::label(cx, "Single Key with Modifier")) .child( div() .flex() .gap_3() - .children(ModifierKey::iter().map(|modifier| { - Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier)) - })), + .child(KeyBinding::new(binding("ctrl-c"))) + .child(KeyBinding::new(binding("alt-c"))) + .child(KeyBinding::new(binding("cmd-c"))) + .child(KeyBinding::new(binding("shift-c"))), ) .child(Story::label(cx, "Single Key with Modifier (Permuted)")) .child( @@ -194,29 +125,17 @@ mod stories { .gap_4() .py_3() .children(chunk.map(|permutation| { - let mut modifiers = ModifierKeys::new(); - - for modifier in permutation { - modifiers = modifiers.add(modifier); - } - - Keybinding::new("X".to_string(), modifiers) + KeyBinding::new(binding(&*(permutation.join("-") + "-x"))) })) }), ), ) .child(Story::label(cx, "Single Key with All Modifiers")) - .child(Keybinding::new("Z".to_string(), ModifierKeys::all())) + .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z"))) .child(Story::label(cx, "Chord")) - .child(Keybinding::new_chord( - ("A".to_string(), ModifierKeys::new()), - ("Z".to_string(), ModifierKeys::new()), - )) + .child(KeyBinding::new(binding("a z"))) .child(Story::label(cx, "Chord with Modifier")) - .child(Keybinding::new_chord( - ("A".to_string(), ModifierKeys::new().control(true)), - ("Z".to_string(), ModifierKeys::new().shift(true)), - )) + .child(KeyBinding::new(binding("ctrl-a shift-z"))) } } } diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 7f736433fc..4e1034595d 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::{h_stack, v_stack, Keybinding, Label, LabelColor}; +use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor}; #[derive(Component)] pub struct Palette { @@ -108,7 +108,7 @@ impl Palette { pub struct PaletteItem { pub label: SharedString, pub sublabel: Option, - pub keybinding: Option, + pub keybinding: Option, } impl PaletteItem { @@ -132,7 +132,7 @@ impl PaletteItem { pub fn keybinding(mut self, keybinding: K) -> Self where - K: Into>, + K: Into>, { self.keybinding = keybinding.into(); self @@ -161,7 +161,7 @@ pub use stories::*; mod stories { use gpui::{Div, Render}; - use crate::{ModifierKeys, Story}; + use crate::{binding, Story}; use super::*; @@ -181,46 +181,24 @@ mod stories { Palette::new("palette-2") .placeholder("Execute a command...") .items(vec![ - PaletteItem::new("theme selector: toggle").keybinding( - Keybinding::new_chord( - ("k".to_string(), ModifierKeys::new().command(true)), - ("t".to_string(), ModifierKeys::new().command(true)), - ), - ), - PaletteItem::new("assistant: inline assist").keybinding( - Keybinding::new( - "enter".to_string(), - ModifierKeys::new().command(true), - ), - ), - PaletteItem::new("assistant: quote selection").keybinding( - Keybinding::new( - ">".to_string(), - ModifierKeys::new().command(true), - ), - ), - PaletteItem::new("assistant: toggle focus").keybinding( - Keybinding::new( - "?".to_string(), - ModifierKeys::new().command(true), - ), - ), + PaletteItem::new("theme selector: toggle") + .keybinding(KeyBinding::new(binding("cmd-k cmd-t"))), + PaletteItem::new("assistant: inline assist") + .keybinding(KeyBinding::new(binding("cmd-enter"))), + PaletteItem::new("assistant: quote selection") + .keybinding(KeyBinding::new(binding("cmd-<"))), + PaletteItem::new("assistant: toggle focus") + .keybinding(KeyBinding::new(binding("cmd-?"))), PaletteItem::new("auto update: check"), PaletteItem::new("auto update: view release notes"), - PaletteItem::new("branches: open recent").keybinding( - Keybinding::new( - "b".to_string(), - ModifierKeys::new().command(true).alt(true), - ), - ), + PaletteItem::new("branches: open recent") + .keybinding(KeyBinding::new(binding("cmd-alt-b"))), PaletteItem::new("chat panel: toggle focus"), PaletteItem::new("cli: install"), PaletteItem::new("client: sign in"), PaletteItem::new("client: sign out"), - PaletteItem::new("editor: cancel").keybinding(Keybinding::new( - "escape".to_string(), - ModifierKeys::new(), - )), + PaletteItem::new("editor: cancel") + .keybinding(KeyBinding::new(binding("escape"))), ]), ) } diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index ffdd3fee98..89aef8140a 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -7,12 +7,12 @@ use gpui::{AppContext, ViewContext}; use rand::Rng; use theme2::ActiveTheme; -use crate::HighlightedText; +use crate::{binding, HighlightedText}; use crate::{ Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus, - HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, - MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus, - PlayerWithCallStatus, PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, + HighlightedLine, Icon, KeyBinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, + MicStatus, Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, + PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, }; use crate::{ListItem, NotificationAction}; @@ -701,46 +701,16 @@ pub fn static_collab_panel_channels() -> Vec { pub fn example_editor_actions() -> Vec { vec![ - PaletteItem::new("New File").keybinding(Keybinding::new( - "N".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Open File").keybinding(Keybinding::new( - "O".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Save File").keybinding(Keybinding::new( - "S".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Cut").keybinding(Keybinding::new( - "X".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Copy").keybinding(Keybinding::new( - "C".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Paste").keybinding(Keybinding::new( - "V".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Undo").keybinding(Keybinding::new( - "Z".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Redo").keybinding(Keybinding::new( - "Z".to_string(), - ModifierKeys::new().command(true).shift(true), - )), - PaletteItem::new("Find").keybinding(Keybinding::new( - "F".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Replace").keybinding(Keybinding::new( - "R".to_string(), - ModifierKeys::new().command(true), - )), + PaletteItem::new("New File").keybinding(KeyBinding::new(binding("cmd-n"))), + PaletteItem::new("Open File").keybinding(KeyBinding::new(binding("cmd-o"))), + PaletteItem::new("Save File").keybinding(KeyBinding::new(binding("cmd-s"))), + PaletteItem::new("Cut").keybinding(KeyBinding::new(binding("cmd-x"))), + PaletteItem::new("Copy").keybinding(KeyBinding::new(binding("cmd-c"))), + PaletteItem::new("Paste").keybinding(KeyBinding::new(binding("cmd-v"))), + PaletteItem::new("Undo").keybinding(KeyBinding::new(binding("cmd-z"))), + PaletteItem::new("Redo").keybinding(KeyBinding::new(binding("cmd-shift-z"))), + PaletteItem::new("Find").keybinding(KeyBinding::new(binding("cmd-f"))), + PaletteItem::new("Replace").keybinding(KeyBinding::new(binding("cmd-r"))), PaletteItem::new("Jump to Line"), PaletteItem::new("Select All"), PaletteItem::new("Deselect All"), From 701f954448ac66084eff4c325038eef0bee8c62d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 17:51:58 -0500 Subject: [PATCH 047/126] Start refining tab --- crates/workspace2/src/pane.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 2bba684d12..008492f178 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1401,20 +1401,27 @@ impl Pane { // .on_drop(|_view, state: View, cx| { // eprintln!("{:?}", state.read(cx)); // }) - .px_2() - .py_0p5() .flex() .items_center() .justify_center() + // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize") + .when(close_right, |this| this.pl_3().pr_1()) + .when(!close_right, |this| this.pr_1().pr_3()) + .py_1() .bg(tab_bg) - .hover(|h| h.bg(tab_hover_bg)) - .active(|a| a.bg(tab_active_bg)) + .border_color(cx.theme().colors().border) + .when(ix < self.active_item_index, |this| this.border_l()) + .when(ix > self.active_item_index, |this| this.border_r()) + .when(ix == self.active_item_index, |this| { + this.border_l().border_r() + }) + // .hover(|h| h.bg(tab_hover_bg)) + // .active(|a| a.bg(tab_active_bg)) .child( div() - .px_1() .flex() .items_center() - .gap_1p5() + .gap_1() .text_color(text_color) .children(if item.has_conflict(cx) { Some( From fea5436ba9322f2afd2a4dc02ef2b00ad29c4637 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 17:54:22 -0500 Subject: [PATCH 048/126] Remove unused imports --- crates/theme2/src/default_theme.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 0e15c1a9af..8502f433f4 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,10 +1,6 @@ -use std::sync::Arc; - use crate::{ - default_color_scales, one_themes::{one_dark, one_family}, - Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors, - ThemeFamily, ThemeStyles, + Theme, ThemeFamily, }; // fn zed_pro_daylight() -> Theme { From 92f2e8eb34c04882076e92ec08cc8d43b42102a4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 13 Nov 2023 18:22:04 -0500 Subject: [PATCH 049/126] Combine related conditions --- crates/workspace2/src/pane.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 008492f178..d0613e13ab 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1405,15 +1405,20 @@ impl Pane { .items_center() .justify_center() // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize") - .when(close_right, |this| this.pl_3().pr_1()) - .when(!close_right, |this| this.pr_1().pr_3()) + .map(|this| { + if close_right { + this.pl_3().pr_1() + } else { + this.pr_1().pr_3() + } + }) .py_1() .bg(tab_bg) .border_color(cx.theme().colors().border) - .when(ix < self.active_item_index, |this| this.border_l()) - .when(ix > self.active_item_index, |this| this.border_r()) - .when(ix == self.active_item_index, |this| { - this.border_l().border_r() + .map(|this| match ix.cmp(&self.active_item_index) { + cmp::Ordering::Less => this.border_l(), + cmp::Ordering::Equal => this.border_r(), + cmp::Ordering::Greater => this.border_l().border_r(), }) // .hover(|h| h.bg(tab_hover_bg)) // .active(|a| a.bg(tab_active_bg)) From aec7955ccf26ee456f5a140c6a678785e864dc10 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 16:40:29 -0700 Subject: [PATCH 050/126] Checkpoint --- crates/gpui2/docs/contexts.md | 0 crates/gpui2/docs/key_dispatch.md | 101 ++++ crates/gpui2/src/app.rs | 2 +- crates/gpui2/src/elements.rs | 1 + crates/gpui2/src/elements/div.rs | 4 +- crates/gpui2/src/elements/node.rs | 767 ++++++++++++++++++++++++++++++ crates/gpui2/src/style.rs | 2 +- crates/gpui2/src/window.rs | 21 + 8 files changed, 894 insertions(+), 4 deletions(-) create mode 100644 crates/gpui2/docs/contexts.md create mode 100644 crates/gpui2/docs/key_dispatch.md create mode 100644 crates/gpui2/src/elements/node.rs diff --git a/crates/gpui2/docs/contexts.md b/crates/gpui2/docs/contexts.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/gpui2/docs/key_dispatch.md b/crates/gpui2/docs/key_dispatch.md new file mode 100644 index 0000000000..339eb43028 --- /dev/null +++ b/crates/gpui2/docs/key_dispatch.md @@ -0,0 +1,101 @@ +# Key Dispatch + +GPUI is designed for keyboard-first interactivity. + +To expose functionality to the mouse, you render a button with a click handler. + +To expose functionality to the keyboard, you bind an *action* in a *key context*. + +Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself: + +```rust +mod menu { + #[gpui::action] + struct MoveUp; + + #[gpui::action] + struct MoveDown; +} +``` + +Actions are frequently unit structs, for which we have a macro. The above could also be written: + +```rust +mod menu { + actions!(MoveUp, MoveDown); +} +``` + +Actions can also be more complex types: + +```rust +mod menu { + #[gpui::action] + struct Move { + direction: Direction, + select: bool, + } +} +``` + +To bind actions, chain `on_action` on to your element: + +```rust +impl Render for Menu { + fn render(&mut self, cx: &mut ViewContext) -> impl Component { + div() + .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext

| { + // ... + }) + .on_action(|this, move: &MoveDown, cx| { + // ... + }) + .children(todo!()) + } +} +``` + +In order to bind keys to actions, you need to declare a *key context* for part of the element tree by calling `key_context`. + +```rust +impl Render for Menu { + fn render(&mut self, cx: &mut ViewContext) -> impl Component { + div() + .key_context("menu") + .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext| { + // ... + }) + .on_action(|this, move: &MoveDown, cx| { + // ... + }) + .children(todo!()) + } +} +``` + +Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name. + +```json +{ + "context": "menu", + "bindings": { + "up": "menu::MoveUp", + "down": "menu::MoveDown" + } +} +``` + +If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name: + +```json +{ + "context": "menu", + "bindings": { + "up": ["menu::Move", {direction: "up", select: false}] + "down": ["menu::Move", {direction: "down", select: false}] + "shift-up": ["menu::Move", {direction: "up", select: true}] + "shift-down": ["menu::Move", {direction: "down", select: true}] + } +} + +``` diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 61c6195d90..1a3d95e761 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1068,7 +1068,7 @@ impl DerefMut for GlobalLease { /// Contains state associated with an active drag operation, started by dragging an element /// within the window or by dragging into the app from the underlying platform. -pub(crate) struct AnyDrag { +pub struct AnyDrag { pub view: AnyView, pub cursor_offset: Point, } diff --git a/crates/gpui2/src/elements.rs b/crates/gpui2/src/elements.rs index eb061f7d34..e0e155fb03 100644 --- a/crates/gpui2/src/elements.rs +++ b/crates/gpui2/src/elements.rs @@ -1,5 +1,6 @@ mod div; mod img; +mod node; mod svg; mod text; mod uniform_list; diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 95c44038ed..080cdc2c4d 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -252,7 +252,7 @@ where cx: &mut ViewContext, ) -> LayoutId { let style = self.compute_style(Bounds::default(), element_state, cx); - style.apply_text_style(cx, |cx| { + style.with_text_style(cx, |cx| { self.with_element_id(cx, |this, _global_id, cx| { let layout_ids = this .children @@ -318,7 +318,7 @@ where ); }); cx.with_z_index(1, |cx| { - style.apply_text_style(cx, |cx| { + style.with_text_style(cx, |cx| { style.apply_overflow(bounds, cx, |cx| { let scroll_offset = element_state.interactive.scroll_offset(); cx.with_element_offset(scroll_offset, |cx| { diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs new file mode 100644 index 0000000000..ec73af8721 --- /dev/null +++ b/crates/gpui2/src/elements/node.rs @@ -0,0 +1,767 @@ +use crate::{ + point, Action, AnyDrag, AnyElement, AnyView, AppContext, BorrowWindow, Bounds, ClickEvent, + DispatchPhase, Element, FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, + ScrollWheelEvent, SharedString, Style, StyleRefinement, Styled, View, ViewContext, Visibility, +}; +use collections::HashMap; +use refineable::Refineable; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + marker::PhantomData, + sync::Arc, +}; + +pub struct GroupStyle { + pub group: SharedString, + pub style: StyleRefinement, +} + +pub trait InteractiveComponent { + fn interactivity(&mut self) -> &mut Interactivity; + + fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().hover_style = f(StyleRefinement::default()); + self + } + + fn group_hover( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self + where + Self: Sized, + { + self.interactivity().group_hover_style = Some(GroupStyle { + group: group_name.into(), + style: f(StyleRefinement::default()), + }); + self + } + + fn on_mouse_down( + mut self, + button: MouseButton, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().mouse_down_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == button + && bounds.contains_point(&event.position) + { + handler(view, event, cx) + } + }, + )); + self + } + + fn on_mouse_up( + mut self, + button: MouseButton, + handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().mouse_up_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == button + && bounds.contains_point(&event.position) + { + handler(view, event, cx) + } + }, + )); + self + } + + fn on_mouse_down_out( + mut self, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().mouse_down_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { + handler(view, event, cx) + } + }, + )); + self + } + + fn on_mouse_up_out( + mut self, + button: MouseButton, + handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().mouse_up_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Capture + && event.button == button + && !bounds.contains_point(&event.position) + { + handler(view, event, cx); + } + }, + )); + self + } + + fn on_mouse_move( + mut self, + handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().mouse_move_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + handler(view, event, cx); + } + }, + )); + self + } + + fn on_scroll_wheel( + mut self, + handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().scroll_wheel_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + handler(view, event, cx); + } + }, + )); + self + } + + /// Capture the given action, fires during the capture phase + fn capture_action( + mut self, + listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().action_listeners.push(( + TypeId::of::(), + Box::new(move |view, action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Capture { + listener(view, action, cx) + } + }), + )); + self + } + + /// Add a listener for the given action, fires during the bubble event phase + fn on_action( + mut self, + listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().action_listeners.push(( + TypeId::of::(), + Box::new(move |view, action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + listener(view, action, cx) + } + }), + )); + self + } + + fn on_key_down( + mut self, + listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity() + .key_down_listeners + .push(Box::new(move |view, event, phase, cx| { + listener(view, event, phase, cx) + })); + self + } + + fn on_key_up( + mut self, + listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity() + .key_up_listeners + .push(Box::new(move |view, event, phase, cx| { + listener(view, event, phase, cx) + })); + self + } + + fn drag_over(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity() + .drag_over_styles + .push((TypeId::of::(), f(StyleRefinement::default()))); + self + } + + fn group_drag_over( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self + where + Self: Sized, + { + self.interactivity().group_drag_over_styles.push(( + TypeId::of::(), + GroupStyle { + group: group_name.into(), + style: f(StyleRefinement::default()), + }, + )); + self + } + + fn on_drop( + mut self, + listener: impl Fn(&mut V, View, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().drop_listeners.push(( + TypeId::of::(), + Box::new(move |view, dragged_view, cx| { + listener(view, dragged_view.downcast().unwrap(), cx); + }), + )); + self + } +} + +pub trait StatefulInteractiveComponent { + fn interactivity(&mut self) -> &mut StatefulInteractivity; + + fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().active_style = f(StyleRefinement::default()); + self + } + + fn group_active( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self + where + Self: Sized, + { + self.interactivity().group_active_style = Some(GroupStyle { + group: group_name.into(), + style: f(StyleRefinement::default()), + }); + self + } + + fn on_click( + mut self, + listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity() + .click_listeners + .push(Box::new(move |view, event, cx| listener(view, event, cx))); + self + } + + fn on_drag( + mut self, + listener: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + ) -> Self + where + Self: Sized, + W: 'static + Render, + { + debug_assert!( + self.interactivity().drag_listener.is_none(), + "calling on_drag more than once on the same element is not supported" + ); + self.interactivity().drag_listener = + Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag { + view: listener(view_state, cx).into(), + cursor_offset, + })); + self + } + + fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext)) -> Self + where + Self: Sized, + { + debug_assert!( + self.interactivity().hover_listener.is_none(), + "calling on_hover more than once on the same element is not supported" + ); + self.interactivity().hover_listener = Some(Box::new(listener)); + self + } + + fn tooltip( + mut self, + build_tooltip: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + ) -> Self + where + Self: Sized, + W: 'static + Render, + { + debug_assert!( + self.interactivity().tooltip_builder.is_none(), + "calling tooltip more than once on the same element is not supported" + ); + self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| { + build_tooltip(view_state, cx).into() + })); + + self + } +} + +pub trait FocusableComponent { + fn focusability(&mut self) -> &mut Focusability; + + fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.focusability().focus_style = f(StyleRefinement::default()); + self + } + + fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.focusability().focus_in_style = f(StyleRefinement::default()); + self + } + + fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + // self.focusability(). (f(StyleRefinement::default())); + self + } + + fn on_focus( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.focusability() + .focus_listeners + .push(Box::new(move |view, focus_handle, event, cx| { + if event.focused.as_ref() == Some(focus_handle) { + listener(view, event, cx) + } + })); + self + } + + fn on_blur( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.focusability() + .focus_listeners + .push(Box::new(move |view, focus_handle, event, cx| { + if event.blurred.as_ref() == Some(focus_handle) { + listener(view, event, cx) + } + })); + self + } + + fn on_focus_in( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.focusability() + .focus_listeners + .push(Box::new(move |view, focus_handle, event, cx| { + let descendant_blurred = event + .blurred + .as_ref() + .map_or(false, |blurred| focus_handle.contains(blurred, cx)); + let descendant_focused = event + .focused + .as_ref() + .map_or(false, |focused| focus_handle.contains(focused, cx)); + + if !descendant_blurred && descendant_focused { + listener(view, event, cx) + } + })); + self + } + + fn on_focus_out( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.focusability() + .focus_listeners + .push(Box::new(move |view, focus_handle, event, cx| { + let descendant_blurred = event + .blurred + .as_ref() + .map_or(false, |blurred| focus_handle.contains(blurred, cx)); + let descendant_focused = event + .focused + .as_ref() + .map_or(false, |focused| focus_handle.contains(focused, cx)); + if descendant_blurred && !descendant_focused { + listener(view, event, cx) + } + })); + self + } +} + +pub type FocusListeners = SmallVec<[FocusListener; 2]>; + +pub type FocusListener = + Box) + 'static>; + +pub type MouseDownListener = Box< + dyn Fn(&mut V, &MouseDownEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, +>; +pub type MouseUpListener = Box< + dyn Fn(&mut V, &MouseUpEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, +>; + +pub type MouseMoveListener = Box< + dyn Fn(&mut V, &MouseMoveEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, +>; + +pub type ScrollWheelListener = Box< + dyn Fn(&mut V, &ScrollWheelEvent, &Bounds, DispatchPhase, &mut ViewContext) + + 'static, +>; + +pub type ClickListener = Box) + 'static>; + +pub type DragListener = + Box, &mut ViewContext) -> AnyDrag + 'static>; + +type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; + +pub type HoverListener = Box) + 'static>; + +pub type TooltipBuilder = Arc) -> AnyView + 'static>; + +pub type KeyDownListener = + Box) + 'static>; + +pub type KeyUpListener = + Box) + 'static>; + +pub type ActionListener = + Box) + 'static>; + +pub struct FocusEvent { + pub blurred: Option, + pub focused: Option, +} + +pub struct Node { + style: StyleRefinement, + key_context: KeyContext, + interactivity: Interactivity, + children: Vec>, +} + +pub struct Interactivity { + group: Option, + pub dispatch_context: KeyContext, + pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, + pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, + pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, + pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, + pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, + pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, + pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, + pub hover_style: StyleRefinement, + pub group_hover_style: Option, + drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, + group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, + drop_listeners: SmallVec<[(TypeId, Box>); 2]>, + scroll_offset: Point, +} + +impl Node { + fn compute_style(&self) -> Style { + let mut style = Style::default(); + style.refine(&self.style); + style + } +} + +impl Styled for Node { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl InteractiveComponent for Node { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +pub struct NodeState { + child_layout_ids: SmallVec<[LayoutId; 4]>, +} + +impl Element for Node { + type ElementState = NodeState; + + fn id(&self) -> Option { + None + } + + fn initialize( + &mut self, + view_state: &mut V, + _: Option, + cx: &mut ViewContext, + ) -> Self::ElementState { + for child in &mut self.children { + child.initialize(view_state, cx); + } + NodeState { + child_layout_ids: SmallVec::new(), + } + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) -> crate::LayoutId { + let style = self.compute_style(); + style.with_text_style(cx, |cx| { + element_state.child_layout_ids = self + .children + .iter_mut() + .map(|child| child.layout(view_state, cx)) + .collect::>(); + cx.request_layout(&style, element_state.child_layout_ids.iter().copied()) + }) + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + let style = self.compute_style(); + if style.visibility == Visibility::Hidden { + return; + } + + if let Some(mouse_cursor) = style.mouse_cursor { + let hovered = bounds.contains_point(&cx.mouse_position()); + if hovered { + cx.set_cursor_style(mouse_cursor); + } + } + + if let Some(group) = self.interactivity.group.clone() { + GroupBounds::push(group, bounds, cx); + } + + let z_index = style.z_index.unwrap_or(0); + + let mut child_min = point(Pixels::MAX, Pixels::MAX); + let mut child_max = Point::default(); + + let content_size = if element_state.child_layout_ids.is_empty() { + bounds.size + } else { + for child_layout_id in &element_state.child_layout_ids { + let child_bounds = cx.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.lower_right()); + } + (child_max - child_min).into() + }; + + cx.with_z_index(z_index, |cx| { + cx.with_z_index(0, |cx| { + style.paint(bounds, cx); + }); + cx.with_z_index(1, |cx| { + style.with_text_style(cx, |cx| { + style.apply_overflow(bounds, cx, |cx| { + let scroll_offset = self.interactivity.scroll_offset; + cx.with_element_offset2(scroll_offset, |cx| { + for child in &mut self.children { + child.paint(view_state, cx); + } + }); + }) + }) + }); + }); + + if let Some(group) = self.interactivity.group.as_ref() { + GroupBounds::pop(group, cx); + } + } +} + +#[derive(Default)] +pub struct GroupBounds(HashMap; 1]>>); + +impl GroupBounds { + pub fn get(name: &SharedString, cx: &mut AppContext) -> Option> { + cx.default_global::() + .0 + .get(name) + .and_then(|bounds_stack| bounds_stack.last()) + .cloned() + } + + pub fn push(name: SharedString, bounds: Bounds, cx: &mut AppContext) { + cx.default_global::() + .0 + .entry(name) + .or_default() + .push(bounds); + } + + pub fn pop(name: &SharedString, cx: &mut AppContext) { + cx.default_global::().0.get_mut(name).unwrap().pop(); + } +} + +pub struct Focusable { + focusability: Focusability, + view_type: PhantomData, + element: E, +} + +pub struct Focusability { + focus_handle: Option, + focus_listeners: FocusListeners, + focus_style: StyleRefinement, + focus_in_style: StyleRefinement, + in_focus_style: StyleRefinement, +} + +impl FocusableComponent for Focusable { + fn focusability(&mut self) -> &mut Focusability { + &mut self.focusability + } +} + +impl> InteractiveComponent for Focusable { + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() + } +} + +impl> StatefulInteractiveComponent + for Focusable +{ + fn interactivity(&mut self) -> &mut StatefulInteractivity { + self.element.interactivity() + } +} + +pub struct Stateful { + id: SharedString, + interactivity: StatefulInteractivity, + view_type: PhantomData, + element: E, +} + +pub struct StatefulInteractivity { + click_listeners: SmallVec<[ClickListener; 2]>, + active_style: StyleRefinement, + group_active_style: Option, + drag_listener: Option>, + hover_listener: Option>, + tooltip_builder: Option>, +} + +impl StatefulInteractiveComponent for Stateful { + fn interactivity(&mut self) -> &mut StatefulInteractivity { + &mut self.interactivity + } +} + +impl> InteractiveComponent for Stateful { + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() + } +} + +impl> FocusableComponent for Stateful { + fn focusability(&mut self) -> &mut Focusability { + self.element.focusability() + } +} diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 664cc61f8a..18b92c0b8b 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -228,7 +228,7 @@ impl Style { } } - pub fn apply_text_style(&self, cx: &mut C, f: F) -> R + pub fn with_text_style(&self, cx: &mut C, f: F) -> R where C: BorrowAppContext, F: FnOnce(&mut C) -> R, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 4a7241a5c5..52c5464e4f 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1587,6 +1587,27 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { result } + /// Update the global element offset based on the given offset. This is used to implement + /// scrolling and position drag handles. + fn with_element_offset2( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + if offset.is_zero() { + return f(self); + }; + + let offset = self.element_offset() + offset; + self.window_mut() + .current_frame + .element_offset_stack + .push(offset); + let result = f(self); + self.window_mut().current_frame.element_offset_stack.pop(); + result + } + /// Obtain the current element offset. fn element_offset(&self) -> Point { self.window() From 0430e8fbf27f0ce597002d3a8e9b6d13835552d9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 13 Nov 2023 18:44:17 -0500 Subject: [PATCH 051/126] Use "One Dark" as default theme --- crates/storybook2/src/storybook2.rs | 2 +- crates/theme2/src/settings.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index f0ba124162..c4c1d75eac 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -48,7 +48,7 @@ fn main() { let args = Args::parse(); let story_selector = args.story.clone(); - let theme_name = args.theme.unwrap_or("Zed Pro Moonlight".to_string()); + let theme_name = args.theme.unwrap_or("One Dark".to_string()); let asset_source = Arc::new(Assets); gpui::App::production(asset_source).run(move |cx| { diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index c575768401..8a15b52641 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -1,3 +1,4 @@ +use crate::one_themes::one_dark; use crate::{Theme, ThemeRegistry}; use anyhow::Result; use gpui::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels}; @@ -129,7 +130,7 @@ impl settings::Settings for ThemeSettings { buffer_line_height: defaults.buffer_line_height.unwrap(), active_theme: themes .get(defaults.theme.as_ref().unwrap()) - .or(themes.get("Zed Pro Moonlight")) + .or(themes.get(&one_dark().name)) .unwrap(), }; From a4e9fea133e94d00ac0768e21eec019eaeabea93 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 13 Nov 2023 15:53:04 -0800 Subject: [PATCH 052/126] WIP co-authored-by: conrad --- Cargo.lock | 3 +- .../collab2/src/tests/channel_buffer_tests.rs | 7 +- crates/collab2/src/tests/editor_tests.rs | 88 +- crates/copilot2/Cargo.toml | 2 +- crates/editor2/Cargo.toml | 1 - crates/editor2/src/editor.rs | 54 +- crates/editor2/src/editor_tests.rs | 570 ++-- crates/editor2/src/element.rs | 31 +- crates/editor2/src/selections_collection.rs | 3 + crates/gpui2/src/app/test_context.rs | 36 +- crates/gpui2/src/platform/test/window.rs | 10 +- crates/gpui2/src/text_system/line_layout.rs | 4 +- crates/project_panel2/src/project_panel.rs | 2868 +++++++++++++++++ crates/workspace2/src/workspace2.rs | 89 +- crates/zed2/src/main.rs | 1 + 15 files changed, 3384 insertions(+), 383 deletions(-) create mode 100644 crates/project_panel2/src/project_panel.rs diff --git a/Cargo.lock b/Cargo.lock index 49f37fb042..97653e124a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2012,7 +2012,7 @@ dependencies = [ "serde_derive", "settings2", "smol", - "theme", + "theme2", "util", ] @@ -2768,7 +2768,6 @@ dependencies = [ "copilot2", "ctor", "db2", - "drag_and_drop", "env_logger 0.9.3", "futures 0.3.28", "fuzzy2", diff --git a/crates/collab2/src/tests/channel_buffer_tests.rs b/crates/collab2/src/tests/channel_buffer_tests.rs index 0da5256682..612832672d 100644 --- a/crates/collab2/src/tests/channel_buffer_tests.rs +++ b/crates/collab2/src/tests/channel_buffer_tests.rs @@ -1,11 +1,14 @@ +use std::ops::Range; + use crate::{ rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, tests::TestServer, }; -use client::{Collaborator, UserId}; +use client::{Collaborator, ParticipantIndex, UserId}; use collections::HashMap; +use editor::{Anchor, Editor, ToOffset}; use futures::future; -use gpui::{BackgroundExecutor, Model, TestAppContext}; +use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; #[gpui::test] diff --git a/crates/collab2/src/tests/editor_tests.rs b/crates/collab2/src/tests/editor_tests.rs index 5d1378a882..8fce187492 100644 --- a/crates/collab2/src/tests/editor_tests.rs +++ b/crates/collab2/src/tests/editor_tests.rs @@ -1,10 +1,30 @@ -use editor::{ - test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, - ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo, +use std::{ + path::Path, + sync::{ + atomic::{self, AtomicBool, AtomicUsize}, + Arc, + }, }; -use gpui::{BackgroundExecutor, TestAppContext}; -use crate::tests::TestServer; +use call::ActiveCall; +use editor::{ + test::editor_test_context::{AssertionContextManager, EditorTestContext}, + Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, + ToggleCodeActions, Undo, +}; +use gpui::{BackgroundExecutor, TestAppContext, VisualContext, VisualTestContext}; +use indoc::indoc; +use language::{ + language_settings::{AllLanguageSettings, InlayHintSettings}, + tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, +}; +use rpc::RECEIVE_TIMEOUT; +use serde_json::json; +use settings::SettingsStore; +use text::Point; +use workspace::Workspace; + +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; #[gpui::test(iterations = 10)] async fn test_host_disconnect( @@ -13,7 +33,7 @@ async fn test_host_disconnect( cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - let mut server = TestServer::start(&executor).await; + let mut server = TestServer::start(executor).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; @@ -27,7 +47,7 @@ async fn test_host_disconnect( .fs() .insert_tree( "/a", - json!({ + serde_json::json!({ "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -37,7 +57,7 @@ async fn test_host_disconnect( let active_call_a = cx_a.read(ActiveCall::global); let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); + let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees().next().unwrap()); let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await @@ -50,19 +70,23 @@ async fn test_host_disconnect( let workspace_b = cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); + let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "b.txt"), None, true, cx) }) + .unwrap() .await .unwrap() .downcast::() .unwrap(); - assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx))); + //TODO: focus + assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx))); editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); - assert!(window_b.is_edited(cx_b)); + //todo(is_edited) + // assert!(workspace_b.is_edited(cx_b)); // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. server.forbid_connections(); @@ -79,10 +103,10 @@ async fn test_host_disconnect( // Ensure client B's edited state is reset and that the whole window is blurred. - window_b.read_with(cx_b, |cx| { + workspace_b.update(cx_b, |_, cx| { assert_eq!(cx.focused_view_id(), None); }); - assert!(!window_b.is_edited(cx_b)); + // assert!(!workspace_b.is_edited(cx_b)); // Ensure client B is not prompted to save edits when closing window after disconnecting. let can_close = workspace_b @@ -153,12 +177,14 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); - let window_a = cx_a.add_window(|_| EmptyView); - let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); + let window_a = cx_a.add_empty_window(); + let editor_a = + window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); let mut editor_cx_a = EditorTestContext { cx: cx_a, window: window_a.into(), editor: editor_a, + assertion_cx: AssertionContextManager::new(), }; // Open a buffer as client B @@ -166,12 +192,14 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); - let window_b = cx_b.add_window(|_| EmptyView); - let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); + let window_b = cx_b.add_empty_window(); + let editor_b = + window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); let mut editor_cx_b = EditorTestContext { cx: cx_b, window: window_b.into(), editor: editor_b, + assertion_cx: AssertionContextManager::new(), }; // Test newline above @@ -275,8 +303,8 @@ async fn test_collaborating_with_completion( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); - let window_b = cx_b.add_window(|_| EmptyView); - let editor_b = window_b.add_view(cx_b, |cx| { + let window_b = cx_b.add_empty_window(); + let editor_b = window_b.build_view(cx_b, |cx| { Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) }); @@ -384,7 +412,7 @@ async fn test_collaborating_with_completion( ); // The additional edit is applied. - cx_a.foreground().run_until_parked(); + cx_a.executor().run_until_parked(); buffer_a.read_with(cx_a, |buffer, _| { assert_eq!( @@ -935,8 +963,8 @@ async fn test_share_project( cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - let window_b = cx_b.add_window(|_| EmptyView); - let mut server = TestServer::start(&executor).await; + let window_b = cx_b.add_empty_window(); + let mut server = TestServer::start(executor).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; @@ -1050,7 +1078,7 @@ async fn test_share_project( .await .unwrap(); - let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); + let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); // Client A sees client B's selection executor.run_until_parked(); @@ -1164,10 +1192,12 @@ async fn test_on_input_format_from_host_to_guest( .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); - let window_a = cx_a.add_window(|_| EmptyView); - let editor_a = window_a.add_view(cx_a, |cx| { - Editor::for_buffer(buffer_a, Some(project_a.clone()), cx) - }); + let window_a = cx_a.add_empty_window(); + let editor_a = window_a + .update(cx_a, |_, cx| { + cx.build_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)) + }) + .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); executor.run_until_parked(); @@ -1294,8 +1324,8 @@ async fn test_on_input_format_from_guest_to_host( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); - let window_b = cx_b.add_window(|_| EmptyView); - let editor_b = window_b.add_view(cx_b, |cx| { + let window_b = cx_b.add_empty_window(); + let editor_b = window_b.build_view(cx_b, |cx| { Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) }); @@ -1459,7 +1489,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .unwrap(); - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root_view(cx_a); cx_a.foreground().start_waiting(); // The host opens a rust file. diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml index 2ce432d9fc..68b56a6c01 100644 --- a/crates/copilot2/Cargo.toml +++ b/crates/copilot2/Cargo.toml @@ -24,7 +24,7 @@ collections = { path = "../collections" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } settings = { package = "settings2", path = "../settings2" } -theme = { path = "../theme" } +theme = { package = "theme2", path = "../theme2" } lsp = { package = "lsp2", path = "../lsp2" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml index e45c33d917..98bd06bc4d 100644 --- a/crates/editor2/Cargo.toml +++ b/crates/editor2/Cargo.toml @@ -27,7 +27,6 @@ client = { package = "client2", path = "../client2" } clock = { path = "../clock" } copilot = { package="copilot2", path = "../copilot2" } db = { package="db2", path = "../db2" } -drag_and_drop = { path = "../drag_and_drop" } collections = { path = "../collections" } # context_menu = { path = "../context_menu" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 891c15580e..e10cc9971e 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -2023,24 +2023,24 @@ impl Editor { dispatch_context } - // pub fn new_file( - // workspace: &mut Workspace, - // _: &workspace::NewFile, - // cx: &mut ViewContext, - // ) { - // let project = workspace.project().clone(); - // if project.read(cx).is_remote() { - // cx.propagate(); - // } else if let Some(buffer) = project - // .update(cx, |project, cx| project.create_buffer("", None, cx)) - // .log_err() - // { - // workspace.add_item( - // Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), - // cx, - // ); - // } - // } + pub fn new_file( + workspace: &mut Workspace, + _: &workspace::NewFile, + cx: &mut ViewContext, + ) { + let project = workspace.project().clone(); + if project.read(cx).is_remote() { + cx.propagate(); + } else if let Some(buffer) = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .log_err() + { + workspace.add_item( + Box::new(cx.build_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), + cx, + ); + } + } // pub fn new_file_in_direction( // workspace: &mut Workspace, @@ -2124,17 +2124,17 @@ impl Editor { // ) // } - // pub fn mode(&self) -> EditorMode { - // self.mode - // } + pub fn mode(&self) -> EditorMode { + self.mode + } - // pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { - // self.collaboration_hub.as_deref() - // } + pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { + self.collaboration_hub.as_deref() + } - // pub fn set_collaboration_hub(&mut self, hub: Box) { - // self.collaboration_hub = Some(hub); - // } + pub fn set_collaboration_hub(&mut self, hub: Box) { + self.collaboration_hub = Some(hub); + } pub fn set_placeholder_text( &mut self, diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index eccf0d6d20..0243d5a0b0 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -7,7 +7,7 @@ use crate::{ }, JoinLines, }; -use drag_and_drop::DragAndDrop; + use futures::StreamExt; use gpui::{ div, @@ -517,7 +517,6 @@ fn test_clone(cx: &mut TestAppContext) { 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.executor()); @@ -3483,198 +3482,256 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { } #[gpui::test] -fn test_add_selection_above_below(cx: &mut TestAppContext) { +async 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) + let mut cx = EditorTestContext::new(cx).await; + + // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); + cx.set_state(indoc!( + r#"abc + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|editor, cx| { + editor.add_selection_above(&Default::default(), cx); }); - 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) - ] - ); + cx.assert_editor_state(indoc!( + r#"abcˇ + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|editor, cx| { + editor.add_selection_above(&Default::default(), cx); }); - 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) - ] - ); + cx.assert_editor_state(indoc!( + r#"abcˇ + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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)] - ); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi - 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) - ] - ); + jk + nlmo + "# + )); - view.redo_selection(&RedoSelection, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] - ); + cx.update_editor(|view, cx| { + view.undo_selection(&Default::default(), cx); }); - 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) - ] - ); + cx.assert_editor_state(indoc!( + r#"abcˇ + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.redo_selection(&Default::default(), cx); }); - 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) - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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) - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi + + jk + nlmˇo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi + + jk + nlmˇo + "# + )); + + // change selections + cx.set_state(indoc!( + r#"abc + def«ˇg»hi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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)] - ); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi + + jk + nlm«ˇo» + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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)] - ); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi + + jk + nlm«ˇo» + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); }); - 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), - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); }); - 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), - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi + + jk + nlmo + "# + )); + + // Change selections again + cx.set_state(indoc!( + r#"a«bc + defgˇ»hi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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), - ] - ); + cx.assert_editor_state(indoc!( + r#"a«bcˇ» + d«efgˇ»hi + + j«kˇ» + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); + }); + cx.assert_editor_state(indoc!( + r#"a«bcˇ» + d«efgˇ»hi + + j«kˇ» + n«lmoˇ» + "# + )); + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); }); - 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), - ] - ); + cx.assert_editor_state(indoc!( + r#"a«bcˇ» + d«efgˇ»hi + + j«kˇ» + nlmo + "# + )); + + // Change selections again + cx.set_state(indoc!( + r#"abc + d«ˇefghi + + jk + nlm»o + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); }); - 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), - ] - ); + cx.assert_editor_state(indoc!( + r#"a«ˇbc» + d«ˇef»ghi + + j«ˇk» + n«ˇlm»o + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); + + cx.assert_editor_state(indoc!( + r#"abc + d«ˇef»ghi + + j«ˇk» + n«ˇlm»o + "# + )); } #[gpui::test] @@ -6898,6 +6955,7 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) &r#" ˇuse some::modified; + fn main() { println!("hello there"); @@ -6919,6 +6977,7 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) &r#" use some::modified; + fn main() { ˇ println!("hello there"); @@ -6958,6 +7017,7 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) &r#" use some::modified; + fn main() { ˇ println!("hello there"); @@ -6981,6 +7041,7 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) &r#" ˇuse some::modified; + fn main() { println!("hello there"); @@ -7374,105 +7435,106 @@ async fn test_copilot_completion_invalidation( }); } -#[gpui::test] -async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); +//todo!() +// #[gpui::test] +// async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); - let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| cx.set_global(copilot)); +// let (copilot, copilot_lsp) = Copilot::fake(cx); +// cx.update(|cx| cx.set_global(copilot)); - let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n")); - let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n")); - let multibuffer = cx.build_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer_1.clone(), - [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)); +// let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n")); +// let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n")); +// let multibuffer = cx.build_model(|cx| { +// let mut multibuffer = MultiBuffer::new(0); +// multibuffer.push_excerpts( +// buffer_1.clone(), +// [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)); - 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); - }); - executor.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: "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); +// }); +// executor.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"); +// 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"); - }); +// // 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. - executor.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"); - }); -} +// // Ensure the new suggestion is displayed when the debounce timeout expires. +// executor.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(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 67fcbaa4ba..abbb06b3ca 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1683,21 +1683,24 @@ impl EditorElement { ShowScrollbar::Never => false, }; - let fold_ranges: Vec<(BufferRow, Range, Hsla)> = fold_ranges - .into_iter() - .map(|(id, fold)| { - todo!("folds!") - // let color = self - // .style - // .folds - // .ellipses - // .background - // .style_for(&mut cx.mouse_state::(id as usize)) - // .color; + let fold_ranges: Vec<(BufferRow, Range, Hsla)> = Vec::new(); + // todo!() - // (id, fold, color) - }) - .collect(); + // fold_ranges + // .into_iter() + // .map(|(id, fold)| { + // // todo!("folds!") + // // let color = self + // // .style + // // .folds + // // .ellipses + // // .background + // // .style_for(&mut cx.mouse_state::(id as usize)) + // // .color; + + // // (id, fold, color) + // }) + // .collect(); let head_for_relative = newest_selection_head.unwrap_or_else(|| { let newest = editor.selections.newest::(cx); diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index 23c5a75809..01e241c830 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -315,11 +315,14 @@ impl SelectionsCollection { let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details); + dbg!("****START COL****"); let start_col = layed_out_line.closest_index_for_x(positions.start) as u32; if start_col < line_len || (is_empty && positions.start == layed_out_line.width) { let start = DisplayPoint::new(row, start_col); + dbg!("****END COL****"); let end_col = layed_out_line.closest_index_for_x(positions.end) as u32; let end = DisplayPoint::new(row, end_col); + dbg!(start_col, end_col); Some(Selection { id: post_inc(&mut self.next_selection_id), diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 2e99477674..44c31bbd69 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,8 @@ use crate::{ - AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, - EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, - Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext, VisualContext, - WindowContext, WindowHandle, WindowOptions, + div, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, + Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, + ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext, + VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -132,6 +132,14 @@ impl TestAppContext { cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)) } + pub fn add_empty_window(&mut self) -> AnyWindowHandle { + let mut cx = self.app.borrow_mut(); + cx.open_window(WindowOptions::default(), |cx| { + cx.build_view(|_| EmptyView {}) + }) + .any_handle + } + pub fn add_window_view(&mut self, build_window: F) -> (View, VisualTestContext) where F: FnOnce(&mut ViewContext) -> V, @@ -456,3 +464,23 @@ impl<'a> VisualContext for VisualTestContext<'a> { .unwrap() } } + +impl AnyWindowHandle { + pub fn build_view( + &self, + cx: &mut TestAppContext, + build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, + ) -> View { + self.update(cx, |_, cx| cx.build_view(build_view)).unwrap() + } +} + +pub struct EmptyView {} + +impl Render for EmptyView { + type Element = Div; + + fn render(&mut self, _cx: &mut crate::ViewContext) -> Self::Element { + div() + } +} diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index 289ecf7e6b..cf9143162e 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -8,7 +8,8 @@ use parking_lot::Mutex; use crate::{ px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, - PlatformWindow, Point, Scene, Size, TileId, WindowAppearance, WindowBounds, WindowOptions, + PlatformInputHandler, PlatformWindow, Point, Scene, Size, TileId, WindowAppearance, + WindowBounds, WindowOptions, }; #[derive(Default)] @@ -23,6 +24,7 @@ pub struct TestWindow { bounds: WindowBounds, current_scene: Mutex>, display: Rc, + input_handler: Option>, handlers: Mutex, sprite_atlas: Arc, @@ -33,7 +35,7 @@ impl TestWindow { bounds: options.bounds, current_scene: Default::default(), display, - + input_handler: None, sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), } @@ -77,8 +79,8 @@ impl PlatformWindow for TestWindow { todo!() } - fn set_input_handler(&mut self, _input_handler: Box) { - todo!() + fn set_input_handler(&mut self, input_handler: Box) { + self.input_handler = Some(input_handler); } fn prompt( diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index 1b5e28c958..db7140b040 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/crates/gpui2/src/text_system/line_layout.rs @@ -54,9 +54,9 @@ impl LineLayout { pub fn closest_index_for_x(&self, x: Pixels) -> usize { let mut prev_index = 0; let mut prev_x = px(0.); - for run in self.runs.iter() { for glyph in run.glyphs.iter() { + glyph.index; if glyph.position.x >= x { if glyph.position.x - x < x - prev_x { return glyph.index; @@ -68,7 +68,7 @@ impl LineLayout { prev_x = glyph.position.x; } } - prev_index + prev_index + 1 } pub fn x_for_index(&self, index: usize) -> Pixels { diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs new file mode 100644 index 0000000000..e3e04f5254 --- /dev/null +++ b/crates/project_panel2/src/project_panel.rs @@ -0,0 +1,2868 @@ +pub mod file_associations; +mod project_panel_settings; +use settings::Settings; + +use db::kvp::KEY_VALUE_STORE; +use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; +use file_associations::FileAssociations; + +use anyhow::{anyhow, Result}; +use gpui::{ + actions, div, px, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, + AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusEnabled, + FocusHandle, Model, ParentElement as _, Pixels, Point, PromptLevel, Render, + StatefulInteractive, StatefulInteractivity, Styled, Task, UniformListScrollHandle, View, + ViewContext, VisualContext as _, WeakView, WindowContext, +}; +use menu::{Confirm, SelectNext, SelectPrev}; +use project::{ + repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, + Worktree, WorktreeId, +}; +use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use std::{ + cmp::Ordering, + collections::{hash_map, HashMap}, + ffi::OsStr, + ops::Range, + path::Path, + sync::Arc, +}; +use theme::ActiveTheme as _; +use ui::{h_stack, v_stack}; +use unicase::UniCase; +use util::TryFutureExt; +use workspace::{ + dock::{DockPosition, PanelEvent}, + Workspace, +}; + +const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; +const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; + +pub struct ProjectPanel { + project: Model, + fs: Arc, + list: UniformListScrollHandle, + focus_handle: FocusHandle, + visible_entries: Vec<(WorktreeId, Vec)>, + last_worktree_root_id: Option, + expanded_dir_ids: HashMap>, + selection: Option, + edit_state: Option, + filename_editor: View, + clipboard_entry: Option, + dragged_entry_destination: Option>, + workspace: WeakView, + has_focus: bool, + width: Option, + pending_serialization: Task>, +} + +#[derive(Copy, Clone, Debug)] +struct Selection { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, +} + +#[derive(Clone, Debug)] +struct EditState { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + is_new_entry: bool, + is_dir: bool, + processing_filename: Option, +} + +#[derive(Copy, Clone)] +pub enum ClipboardEntry { + Copied { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + }, + Cut { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + }, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct EntryDetails { + filename: String, + icon: Option>, + path: Arc, + depth: usize, + kind: EntryKind, + is_ignored: bool, + is_expanded: bool, + is_selected: bool, + is_editing: bool, + is_processing: bool, + is_cut: bool, + git_status: Option, +} + +actions!( + ExpandSelectedEntry, + CollapseSelectedEntry, + CollapseAllEntries, + NewDirectory, + NewFile, + Copy, + CopyPath, + CopyRelativePath, + RevealInFinder, + OpenInTerminal, + Cut, + Paste, + Delete, + Rename, + Open, + ToggleFocus, + NewSearchInDirectory, +); + +pub fn init_settings(cx: &mut AppContext) { + ProjectPanelSettings::register(cx); +} + +pub fn init(assets: impl AssetSource, cx: &mut AppContext) { + init_settings(cx); + file_associations::init(assets, cx); + + // cx.add_action(ProjectPanel::expand_selected_entry); + // cx.add_action(ProjectPanel::collapse_selected_entry); + // cx.add_action(ProjectPanel::collapse_all_entries); + // cx.add_action(ProjectPanel::select_prev); + // cx.add_action(ProjectPanel::select_next); + // cx.add_action(ProjectPanel::new_file); + // cx.add_action(ProjectPanel::new_directory); + // cx.add_action(ProjectPanel::rename); + // cx.add_async_action(ProjectPanel::delete); + // cx.add_async_action(ProjectPanel::confirm); + // cx.add_async_action(ProjectPanel::open_file); + // cx.add_action(ProjectPanel::cancel); + // cx.add_action(ProjectPanel::cut); + // cx.add_action(ProjectPanel::copy); + // cx.add_action(ProjectPanel::copy_path); + // cx.add_action(ProjectPanel::copy_relative_path); + // cx.add_action(ProjectPanel::reveal_in_finder); + // cx.add_action(ProjectPanel::open_in_terminal); + // cx.add_action(ProjectPanel::new_search_in_directory); + // cx.add_action( + // |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { + // this.paste(action, cx); + // }, + // ); +} + +#[derive(Debug)] +pub enum Event { + OpenedEntry { + entry_id: ProjectEntryId, + focus_opened_item: bool, + }, + SplitEntry { + entry_id: ProjectEntryId, + }, + DockPositionChanged, + Focus, + NewSearchInDirectory { + dir_entry: Entry, + }, + ActivatePanel, +} + +#[derive(Serialize, Deserialize)] +struct SerializedProjectPanel { + width: Option, +} + +impl ProjectPanel { + fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { + let project = workspace.project().clone(); + let project_panel = cx.build_view(|cx: &mut ViewContext| { + cx.observe(&project, |this, _, cx| { + this.update_visible_entries(None, cx); + cx.notify(); + }) + .detach(); + let focus_handle = cx.focus_handle(); + + cx.on_focus(&focus_handle, Self::focus_in).detach(); + cx.on_blur(&focus_handle, Self::focus_out).detach(); + + cx.subscribe(&project, |this, project, event, cx| match event { + project::Event::ActiveEntryChanged(Some(entry_id)) => { + if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx) + { + this.expand_entry(worktree_id, *entry_id, cx); + this.update_visible_entries(Some((worktree_id, *entry_id)), cx); + this.autoscroll(cx); + cx.notify(); + } + } + project::Event::ActivateProjectPanel => { + cx.emit(Event::ActivatePanel); + } + project::Event::WorktreeRemoved(id) => { + this.expanded_dir_ids.remove(id); + this.update_visible_entries(None, cx); + cx.notify(); + } + _ => {} + }) + .detach(); + + let filename_editor = cx.build_view(|cx| Editor::single_line(cx)); + + cx.subscribe(&filename_editor, |this, _, event, cx| match event { + editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => { + this.autoscroll(cx); + } + _ => {} + }) + .detach(); + + // cx.observe_focus(&filename_editor, |this, _, is_focused, cx| { + // if !is_focused + // && this + // .edit_state + // .as_ref() + // .map_or(false, |state| state.processing_filename.is_none()) + // { + // this.edit_state = None; + // this.update_visible_entries(None, cx); + // } + // }) + // .detach(); + + // cx.observe_global::(|_, cx| { + // cx.notify(); + // }) + // .detach(); + + let view_id = cx.view().entity_id(); + let mut this = Self { + project: project.clone(), + fs: workspace.app_state().fs.clone(), + list: UniformListScrollHandle::new(), + focus_handle, + visible_entries: Default::default(), + last_worktree_root_id: Default::default(), + expanded_dir_ids: Default::default(), + selection: None, + edit_state: None, + filename_editor, + clipboard_entry: None, + // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), + dragged_entry_destination: None, + workspace: workspace.weak_handle(), + has_focus: false, + width: None, + pending_serialization: Task::ready(None), + }; + this.update_visible_entries(None, cx); + + // Update the dock position when the setting changes. + // todo!() + // let mut old_dock_position = this.position(cx); + // cx.observe_global::(move |this, cx| { + // let new_dock_position = this.position(cx); + // if new_dock_position != old_dock_position { + // old_dock_position = new_dock_position; + // cx.emit(Event::DockPositionChanged); + // } + // }) + // .detach(); + + this + }); + + cx.subscribe(&project_panel, { + let project_panel = project_panel.downgrade(); + move |workspace, _, event, cx| match event { + &Event::OpenedEntry { + entry_id, + focus_opened_item, + } => { + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { + if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + workspace + .open_path( + ProjectPath { + worktree_id: worktree.read(cx).id(), + path: entry.path.clone(), + }, + None, + focus_opened_item, + cx, + ) + .detach_and_log_err(cx); + if !focus_opened_item { + if let Some(project_panel) = project_panel.upgrade() { + let focus_handle = project_panel.read(cx).focus_handle.clone(); + cx.focus(&focus_handle); + } + } + } + } + } + &Event::SplitEntry { entry_id } => { + // if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { + // if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + // workspace + // .split_path( + // ProjectPath { + // worktree_id: worktree.read(cx).id(), + // path: entry.path.clone(), + // }, + // cx, + // ) + // .detach_and_log_err(cx); + // } + // } + } + _ => {} + } + }) + .detach(); + + project_panel + } + + pub fn load( + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + // let serialized_panel = if let Some(panel) = cx + // .background_executor() + // .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) }) + // .await + // .log_err() + // .flatten() + // { + // Some(serde_json::from_str::(&panel)?) + // } else { + // None + // }; + workspace.update(&mut cx, |workspace, cx| { + let panel = ProjectPanel::new(workspace, cx); + // if let Some(serialized_panel) = serialized_panel { + // panel.update(cx, |panel, cx| { + // panel.width = serialized_panel.width; + // cx.notify(); + // }); + // } + panel + }) + }) + } + + fn serialize(&mut self, cx: &mut ViewContext) { + let width = self.width; + self.pending_serialization = cx.background_executor().spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + PROJECT_PANEL_KEY.into(), + serde_json::to_string(&SerializedProjectPanel { width })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ); + } + + fn focus_in(&mut self, cx: &mut ViewContext) { + if !self.has_focus { + self.has_focus = true; + cx.emit(Event::Focus); + } + } + + fn focus_out(&mut self, _: &mut ViewContext) { + self.has_focus = false; + } + + fn deploy_context_menu( + &mut self, + position: Point, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) { + // let project = self.project.read(cx); + + // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { + // id + // } else { + // return; + // }; + + // self.selection = Some(Selection { + // worktree_id, + // entry_id, + // }); + + // let mut menu_entries = Vec::new(); + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let is_root = Some(entry) == worktree.root_entry(); + // if !project.is_remote() { + // menu_entries.push(ContextMenuItem::action( + // "Add Folder to Project", + // workspace::AddFolderToProject, + // )); + // if is_root { + // let project = self.project.clone(); + // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { + // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); + // })); + // } + // } + // menu_entries.push(ContextMenuItem::action("New File", NewFile)); + // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Cut", Cut)); + // menu_entries.push(ContextMenuItem::action("Copy", Copy)); + // if let Some(clipboard_entry) = self.clipboard_entry { + // if clipboard_entry.worktree_id() == worktree.id() { + // menu_entries.push(ContextMenuItem::action("Paste", Paste)); + // } + // } + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); + // menu_entries.push(ContextMenuItem::action( + // "Copy Relative Path", + // CopyRelativePath, + // )); + + // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::Separator); + // } + // menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); + // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); + // menu_entries.push(ContextMenuItem::action( + // "Search Inside", + // NewSearchInDirectory, + // )); + // } + + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Rename", Rename)); + // if !is_root { + // menu_entries.push(ContextMenuItem::action("Delete", Delete)); + // } + // } + + // // self.context_menu.update(cx, |menu, cx| { + // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); + // // }); + + // cx.notify(); + } + + fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + if entry.is_dir() { + let worktree_id = worktree.id(); + let entry_id = entry.id; + let expanded_dir_ids = + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + expanded_dir_ids + } else { + return; + }; + + match expanded_dir_ids.binary_search(&entry_id) { + Ok(_) => self.select_next(&SelectNext, cx), + Err(ix) => { + self.project.update(cx, |project, cx| { + project.expand_entry(worktree_id, entry_id, cx); + }); + + expanded_dir_ids.insert(ix, entry_id); + self.update_visible_entries(None, cx); + cx.notify(); + } + } + } + } + } + + fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext) { + if let Some((worktree, mut entry)) = self.selected_entry(cx) { + let worktree_id = worktree.id(); + let expanded_dir_ids = + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + expanded_dir_ids + } else { + return; + }; + + loop { + let entry_id = entry.id; + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.notify(); + break; + } + Err(_) => { + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } + } + } + } + } + } + + pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext) { + self.expanded_dir_ids.clear(); + self.update_visible_entries(None, cx); + cx.notify(); + } + + fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + self.project.update(cx, |project, cx| { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + } + Err(ix) => { + project.expand_entry(worktree_id, entry_id, cx); + expanded_dir_ids.insert(ix, entry_id); + } + } + }); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.focus(&self.focus_handle); + cx.notify(); + } + } + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + let (mut worktree_ix, mut entry_ix, _) = + self.index_for_selection(selection).unwrap_or_default(); + if entry_ix > 0 { + entry_ix -= 1; + } else if worktree_ix > 0 { + worktree_ix -= 1; + entry_ix = self.visible_entries[worktree_ix].1.len() - 1; + } else { + return; + } + + let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix]; + self.selection = Some(Selection { + worktree_id: *worktree_id, + entry_id: worktree_entries[entry_ix].id, + }); + self.autoscroll(cx); + cx.notify(); + } else { + self.select_first(cx); + } + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { + if let Some(task) = self.confirm_edit(cx) { + return Some(task); + } + + None + } + + fn open_file(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { + if let Some((_, entry)) = self.selected_entry(cx) { + if entry.is_file() { + self.open_entry(entry.id, true, cx); + } + } + + None + } + + fn confirm_edit(&mut self, cx: &mut ViewContext) -> Option>> { + let edit_state = self.edit_state.as_mut()?; + cx.focus(&self.focus_handle); + + let worktree_id = edit_state.worktree_id; + let is_new_entry = edit_state.is_new_entry; + let is_dir = edit_state.is_dir; + let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; + let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone(); + let filename = self.filename_editor.read(cx).text(cx); + + let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some(); + let edit_task; + let edited_entry_id; + if is_new_entry { + self.selection = Some(Selection { + worktree_id, + entry_id: NEW_ENTRY_ID, + }); + let new_path = entry.path.join(&filename.trim_start_matches("/")); + if path_already_exists(new_path.as_path()) { + return None; + } + + edited_entry_id = NEW_ENTRY_ID; + edit_task = self.project.update(cx, |project, cx| { + project.create_entry((worktree_id, &new_path), is_dir, cx) + })?; + } else { + let new_path = if let Some(parent) = entry.path.clone().parent() { + parent.join(&filename) + } else { + filename.clone().into() + }; + if path_already_exists(new_path.as_path()) { + return None; + } + + edited_entry_id = entry.id; + edit_task = self.project.update(cx, |project, cx| { + project.rename_entry(entry.id, new_path.as_path(), cx) + })?; + }; + + edit_state.processing_filename = Some(filename); + cx.notify(); + + Some(cx.spawn(|this, mut cx| async move { + let new_entry = edit_task.await; + this.update(&mut cx, |this, cx| { + this.edit_state.take(); + cx.notify(); + })?; + + let new_entry = new_entry?; + this.update(&mut cx, |this, cx| { + if let Some(selection) = &mut this.selection { + if selection.entry_id == edited_entry_id { + selection.worktree_id = worktree_id; + selection.entry_id = new_entry.id; + this.expand_to_selection(cx); + } + } + this.update_visible_entries(None, cx); + if is_new_entry && !is_dir { + this.open_entry(new_entry.id, true, cx); + } + cx.notify(); + })?; + Ok(()) + })) + } + + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + self.edit_state = None; + self.update_visible_entries(None, cx); + cx.focus(&self.focus_handle); + cx.notify(); + } + + fn open_entry( + &mut self, + entry_id: ProjectEntryId, + focus_opened_item: bool, + cx: &mut ViewContext, + ) { + cx.emit(Event::OpenedEntry { + entry_id, + focus_opened_item, + }); + } + + fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { + cx.emit(Event::SplitEntry { entry_id }); + } + + fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext) { + self.add_entry(false, cx) + } + + fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext) { + self.add_entry(true, cx) + } + + fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext) { + if let Some(Selection { + worktree_id, + entry_id, + }) = self.selection + { + let directory_id; + if let Some((worktree, expanded_dir_ids)) = self + .project + .read(cx) + .worktree_for_id(worktree_id, cx) + .zip(self.expanded_dir_ids.get_mut(&worktree_id)) + { + let worktree = worktree.read(cx); + if let Some(mut entry) = worktree.entry_for_id(entry_id) { + loop { + if entry.is_dir() { + if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(ix, entry.id); + } + directory_id = entry.id; + break; + } else { + if let Some(parent_path) = entry.path.parent() { + if let Some(parent_entry) = worktree.entry_for_path(parent_path) { + entry = parent_entry; + continue; + } + } + return; + } + } + } else { + return; + }; + } else { + return; + }; + + self.edit_state = Some(EditState { + worktree_id, + entry_id: directory_id, + is_new_entry: true, + is_dir, + processing_filename: None, + }); + self.filename_editor.update(cx, |editor, cx| { + editor.clear(cx); + editor.focus(cx); + }); + self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx); + self.autoscroll(cx); + cx.notify(); + } + } + + fn rename(&mut self, _: &Rename, cx: &mut ViewContext) { + if let Some(Selection { + worktree_id, + entry_id, + }) = self.selection + { + if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { + if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + self.edit_state = Some(EditState { + worktree_id, + entry_id, + is_new_entry: false, + is_dir: entry.is_dir(), + processing_filename: None, + }); + let file_name = entry + .path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_default() + .to_string(); + let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy()); + let selection_end = + file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); + self.filename_editor.update(cx, |editor, cx| { + editor.set_text(file_name, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([0..selection_end]) + }); + editor.focus(cx); + }); + self.update_visible_entries(None, cx); + self.autoscroll(cx); + cx.notify(); + } + } + + // cx.update_global(|drag_and_drop: &mut DragAndDrop, cx| { + // drag_and_drop.cancel_dragging::(cx); + // }) + } + } + + fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { + let Selection { entry_id, .. } = self.selection?; + let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; + let file_name = path.file_name()?; + + let mut answer = cx.prompt( + PromptLevel::Info, + &format!("Delete {file_name:?}?"), + &["Delete", "Cancel"], + ); + Some(cx.spawn(|this, mut cx| async move { + if answer.await != Ok(0) { + return Ok(()); + } + this.update(&mut cx, |this, cx| { + this.project + .update(cx, |project, cx| project.delete_entry(entry_id, cx)) + .ok_or_else(|| anyhow!("no such entry")) + })?? + .await + })) + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + let (mut worktree_ix, mut entry_ix, _) = + self.index_for_selection(selection).unwrap_or_default(); + if let Some((_, worktree_entries)) = self.visible_entries.get(worktree_ix) { + if entry_ix + 1 < worktree_entries.len() { + entry_ix += 1; + } else { + worktree_ix += 1; + entry_ix = 0; + } + } + + if let Some((worktree_id, worktree_entries)) = self.visible_entries.get(worktree_ix) { + if let Some(entry) = worktree_entries.get(entry_ix) { + self.selection = Some(Selection { + worktree_id: *worktree_id, + entry_id: entry.id, + }); + self.autoscroll(cx); + cx.notify(); + } + } + } else { + self.select_first(cx); + } + } + + fn select_first(&mut self, cx: &mut ViewContext) { + let worktree = self + .visible_entries + .first() + .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx)); + if let Some(worktree) = worktree { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + if let Some(root_entry) = worktree.root_entry() { + self.selection = Some(Selection { + worktree_id, + entry_id: root_entry.id, + }); + self.autoscroll(cx); + cx.notify(); + } + } + } + + fn autoscroll(&mut self, cx: &mut ViewContext) { + if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { + self.list.scroll_to_item(index); + cx.notify(); + } + } + + fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + self.clipboard_entry = Some(ClipboardEntry::Cut { + worktree_id: worktree.id(), + entry_id: entry.id, + }); + cx.notify(); + } + } + + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + self.clipboard_entry = Some(ClipboardEntry::Copied { + worktree_id: worktree.id(), + entry_id: entry.id, + }); + cx.notify(); + } + } + + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) -> Option<()> { + if let Some((worktree, entry)) = self.selected_entry(cx) { + let clipboard_entry = self.clipboard_entry?; + if clipboard_entry.worktree_id() != worktree.id() { + return None; + } + + let clipboard_entry_file_name = self + .project + .read(cx) + .path_for_entry(clipboard_entry.entry_id(), cx)? + .path + .file_name()? + .to_os_string(); + + let mut new_path = entry.path.to_path_buf(); + if entry.is_file() { + new_path.pop(); + } + + new_path.push(&clipboard_entry_file_name); + let extension = new_path.extension().map(|e| e.to_os_string()); + let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?; + let mut ix = 0; + while worktree.entry_for_path(&new_path).is_some() { + new_path.pop(); + + let mut new_file_name = file_name_without_extension.to_os_string(); + new_file_name.push(" copy"); + if ix > 0 { + new_file_name.push(format!(" {}", ix)); + } + if let Some(extension) = extension.as_ref() { + new_file_name.push("."); + new_file_name.push(extension); + } + + new_path.push(new_file_name); + ix += 1; + } + + if clipboard_entry.is_cut() { + if let Some(task) = self.project.update(cx, |project, cx| { + project.rename_entry(clipboard_entry.entry_id(), new_path, cx) + }) { + task.detach_and_log_err(cx) + } + } else if let Some(task) = self.project.update(cx, |project, cx| { + project.copy_entry(clipboard_entry.entry_id(), new_path, cx) + }) { + task.detach_and_log_err(cx) + } + } + None + } + + fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + cx.write_to_clipboard(ClipboardItem::new( + worktree + .abs_path() + .join(&entry.path) + .to_string_lossy() + .to_string(), + )); + } + } + + fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext) { + if let Some((_, entry)) = self.selected_entry(cx) { + cx.write_to_clipboard(ClipboardItem::new(entry.path.to_string_lossy().to_string())); + } + } + + fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + cx.reveal_path(&worktree.abs_path().join(&entry.path)); + } + } + + fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { + todo!() + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let window = cx.window(); + // let view_id = cx.view_id(); + // let path = worktree.abs_path().join(&entry.path); + + // cx.app_context() + // .spawn(|mut cx| async move { + // window.dispatch_action( + // view_id, + // &workspace::OpenTerminal { + // working_directory: path, + // }, + // &mut cx, + // ); + // }) + // .detach(); + // } + } + + pub fn new_search_in_directory( + &mut self, + _: &NewSearchInDirectory, + cx: &mut ViewContext, + ) { + if let Some((_, entry)) = self.selected_entry(cx) { + if entry.is_dir() { + cx.emit(Event::NewSearchInDirectory { + dir_entry: entry.clone(), + }); + } + } + } + + fn move_entry( + &mut self, + entry_to_move: ProjectEntryId, + destination: ProjectEntryId, + destination_is_file: bool, + cx: &mut ViewContext, + ) { + let destination_worktree = self.project.update(cx, |project, cx| { + let entry_path = project.path_for_entry(entry_to_move, cx)?; + let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + + let mut destination_path = destination_entry_path.as_ref(); + if destination_is_file { + destination_path = destination_path.parent()?; + } + + let mut new_path = destination_path.to_path_buf(); + new_path.push(entry_path.path.file_name()?); + if new_path != entry_path.path.as_ref() { + let task = project.rename_entry(entry_to_move, new_path, cx)?; + cx.foreground_executor().spawn(task).detach_and_log_err(cx); + } + + Some(project.worktree_id_for_entry(destination, cx)?) + }); + + if let Some(destination_worktree) = destination_worktree { + self.expand_entry(destination_worktree, destination, cx); + } + } + + fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { + let mut entry_index = 0; + let mut visible_entries_index = 0; + for (worktree_index, (worktree_id, worktree_entries)) in + self.visible_entries.iter().enumerate() + { + if *worktree_id == selection.worktree_id { + for entry in worktree_entries { + if entry.id == selection.entry_id { + return Some((worktree_index, entry_index, visible_entries_index)); + } else { + visible_entries_index += 1; + entry_index += 1; + } + } + break; + } else { + visible_entries_index += worktree_entries.len(); + } + } + None + } + + pub fn selected_entry<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(&'a Worktree, &'a project::Entry)> { + let (worktree, entry) = self.selected_entry_handle(cx)?; + Some((worktree.read(cx), entry)) + } + + fn selected_entry_handle<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(Model, &'a project::Entry)> { + let selection = self.selection?; + let project = self.project.read(cx); + let worktree = project.worktree_for_id(selection.worktree_id, cx)?; + let entry = worktree.read(cx).entry_for_id(selection.entry_id)?; + Some((worktree, entry)) + } + + fn expand_to_selection(&mut self, cx: &mut ViewContext) -> Option<()> { + let (worktree, entry) = self.selected_entry(cx)?; + let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default(); + + for path in entry.path.ancestors() { + let Some(entry) = worktree.entry_for_path(path) else { + continue; + }; + if entry.is_dir() { + if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(idx, entry.id); + } + } + } + + Some(()) + } + + fn update_visible_entries( + &mut self, + new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, + cx: &mut ViewContext, + ) { + let project = self.project.read(cx); + self.last_worktree_root_id = project + .visible_worktrees(cx) + .rev() + .next() + .and_then(|worktree| worktree.read(cx).root_entry()) + .map(|entry| entry.id); + + self.visible_entries.clear(); + for worktree in project.visible_worktrees(cx) { + let snapshot = worktree.read(cx).snapshot(); + let worktree_id = snapshot.id(); + + let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) { + hash_map::Entry::Occupied(e) => e.into_mut(), + hash_map::Entry::Vacant(e) => { + // The first time a worktree's root entry becomes available, + // mark that root entry as expanded. + if let Some(entry) = snapshot.root_entry() { + e.insert(vec![entry.id]).as_slice() + } else { + &[] + } + } + }; + + let mut new_entry_parent_id = None; + let mut new_entry_kind = EntryKind::Dir; + if let Some(edit_state) = &self.edit_state { + if edit_state.worktree_id == worktree_id && edit_state.is_new_entry { + new_entry_parent_id = Some(edit_state.entry_id); + new_entry_kind = if edit_state.is_dir { + EntryKind::Dir + } else { + EntryKind::File(Default::default()) + }; + } + } + + let mut visible_worktree_entries = Vec::new(); + let mut entry_iter = snapshot.entries(true); + + while let Some(entry) = entry_iter.entry() { + visible_worktree_entries.push(entry.clone()); + if Some(entry.id) == new_entry_parent_id { + visible_worktree_entries.push(Entry { + id: NEW_ENTRY_ID, + kind: new_entry_kind, + path: entry.path.join("\0").into(), + inode: 0, + mtime: entry.mtime, + is_symlink: false, + is_ignored: false, + is_external: false, + git_status: entry.git_status, + }); + } + if expanded_dir_ids.binary_search(&entry.id).is_err() + && entry_iter.advance_to_sibling() + { + continue; + } + entry_iter.advance(); + } + + snapshot.propagate_git_statuses(&mut visible_worktree_entries); + + visible_worktree_entries.sort_by(|entry_a, entry_b| { + let mut components_a = entry_a.path.components().peekable(); + let mut components_b = entry_b.path.components().peekable(); + loop { + match (components_a.next(), components_b.next()) { + (Some(component_a), Some(component_b)) => { + let a_is_file = components_a.peek().is_none() && entry_a.is_file(); + let b_is_file = components_b.peek().is_none() && entry_b.is_file(); + let ordering = a_is_file.cmp(&b_is_file).then_with(|| { + let name_a = + UniCase::new(component_a.as_os_str().to_string_lossy()); + let name_b = + UniCase::new(component_b.as_os_str().to_string_lossy()); + name_a.cmp(&name_b) + }); + if !ordering.is_eq() { + return ordering; + } + } + (Some(_), None) => break Ordering::Greater, + (None, Some(_)) => break Ordering::Less, + (None, None) => break Ordering::Equal, + } + } + }); + self.visible_entries + .push((worktree_id, visible_worktree_entries)); + } + + if let Some((worktree_id, entry_id)) = new_selected_entry { + self.selection = Some(Selection { + worktree_id, + entry_id, + }); + } + } + + fn expand_entry( + &mut self, + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) { + self.project.update(cx, |project, cx| { + if let Some((worktree, expanded_dir_ids)) = project + .worktree_for_id(worktree_id, cx) + .zip(self.expanded_dir_ids.get_mut(&worktree_id)) + { + project.expand_entry(worktree_id, entry_id, cx); + let worktree = worktree.read(cx); + + if let Some(mut entry) = worktree.entry_for_id(entry_id) { + loop { + if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(ix, entry.id); + } + + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } + } + } + } + }); + } + + fn for_each_visible_entry( + &self, + range: Range, + cx: &mut ViewContext, + mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext), + ) { + let mut ix = 0; + for (worktree_id, visible_worktree_entries) in &self.visible_entries { + if ix >= range.end { + return; + } + + if ix + visible_worktree_entries.len() <= range.start { + ix += visible_worktree_entries.len(); + continue; + } + + let end_ix = range.end.min(ix + visible_worktree_entries.len()); + let (git_status_setting, show_file_icons, show_folder_icons) = { + let settings = ProjectPanelSettings::get_global(cx); + ( + settings.git_status, + settings.file_icons, + settings.folder_icons, + ) + }; + if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { + let snapshot = worktree.read(cx).snapshot(); + let root_name = OsStr::new(snapshot.root_name()); + let expanded_entry_ids = self + .expanded_dir_ids + .get(&snapshot.id()) + .map(Vec::as_slice) + .unwrap_or(&[]); + + let entry_range = range.start.saturating_sub(ix)..end_ix - ix; + for entry in visible_worktree_entries[entry_range].iter() { + let status = git_status_setting.then(|| entry.git_status).flatten(); + let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); + let icon = match entry.kind { + EntryKind::File(_) => { + if show_file_icons { + Some(FileAssociations::get_icon(&entry.path, cx)) + } else { + None + } + } + _ => { + if show_folder_icons { + Some(FileAssociations::get_folder_icon(is_expanded, cx)) + } else { + Some(FileAssociations::get_chevron_icon(is_expanded, cx)) + } + } + }; + + let mut details = EntryDetails { + filename: entry + .path + .file_name() + .unwrap_or(root_name) + .to_string_lossy() + .to_string(), + icon, + path: entry.path.clone(), + depth: entry.path.components().count(), + kind: entry.kind, + is_ignored: entry.is_ignored, + is_expanded, + is_selected: self.selection.map_or(false, |e| { + e.worktree_id == snapshot.id() && e.entry_id == entry.id + }), + is_editing: false, + is_processing: false, + is_cut: self + .clipboard_entry + .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), + git_status: status, + }; + + if let Some(edit_state) = &self.edit_state { + let is_edited_entry = if edit_state.is_new_entry { + entry.id == NEW_ENTRY_ID + } else { + entry.id == edit_state.entry_id + }; + + if is_edited_entry { + if let Some(processing_filename) = &edit_state.processing_filename { + details.is_processing = true; + details.filename.clear(); + details.filename.push_str(processing_filename); + } else { + if edit_state.is_new_entry { + details.filename.clear(); + } + details.is_editing = true; + } + } + } + + callback(entry.id, details, cx); + } + } + ix = end_ix; + } + } + + fn render_entry_visual_element( + details: &EntryDetails, + editor: Option<&View>, + padding: Pixels, + cx: &mut ViewContext, + ) -> Div { + let show_editor = details.is_editing && !details.is_processing; + + let theme = cx.theme(); + let filename_text_color = details + .git_status + .as_ref() + .map(|status| match status { + GitFileStatus::Added => theme.styles.status.created, + GitFileStatus::Modified => theme.styles.status.modified, + GitFileStatus::Conflict => theme.styles.status.conflict, + }) + .unwrap_or(theme.styles.status.info); + + h_stack() + .child(if let Some(icon) = &details.icon { + div().child(svg().path(icon.to_string())) + } else { + div() + }) + .child( + if let (Some(editor), true) = (editor, show_editor) { + div().child(editor.clone()) + } else { + div().child(details.filename.clone()) + } + .ml_1(), + ) + .pl(padding) + } + + fn render_entry( + entry_id: ProjectEntryId, + details: EntryDetails, + editor: &View, + // dragged_entry_destination: &mut Option>, + // theme: &theme::ProjectPanel, + cx: &mut ViewContext, + ) -> Div> { + let kind = details.kind; + let settings = ProjectPanelSettings::get_global(cx); + const INDENT_SIZE: Pixels = px(16.0); + let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); + let show_editor = details.is_editing && !details.is_processing; + + Self::render_entry_visual_element(&details, Some(editor), padding, cx) + .id(entry_id.to_proto() as usize) + .on_click(move |this, event, cx| { + if !show_editor { + if kind.is_dir() { + this.toggle_expanded(entry_id, cx); + } else { + if event.down.modifiers.command { + this.split_entry(entry_id, cx); + } else { + this.open_entry(entry_id, event.up.click_count > 1, cx); + } + } + } + }) + // .on_down(MouseButton::Right, move |event, this, cx| { + // this.deploy_context_menu(event.position, entry_id, cx); + // }) + // .on_up(MouseButton::Left, move |_, this, cx| { + // if let Some((_, dragged_entry)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // this.move_entry( + // *dragged_entry, + // entry_id, + // matches!(details.kind, EntryKind::File(_)), + // cx, + // ); + // } + // }) + } +} + +impl Render for ProjectPanel { + type Element = Div, FocusEnabled>; + + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { + enum ProjectPanel {} + let theme = cx.theme(); + let last_worktree_root_id = self.last_worktree_root_id; + + let has_worktree = self.visible_entries.len() != 0; + + if has_worktree { + div() + .id("project-panel") + .track_focus(&self.focus_handle) + .child( + uniform_list( + "entries", + self.visible_entries + .iter() + .map(|(_, worktree_entries)| worktree_entries.len()) + .sum(), + |this: &mut Self, range, cx| { + let mut items = SmallVec::new(); + this.for_each_visible_entry(range, cx, |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + // &mut dragged_entry_destination, + cx, + )); + }); + items + }, + ) + .track_scroll(self.list.clone()), + ) + } else { + v_stack() + .id("empty-project_panel") + .track_focus(&self.focus_handle) + } + } +} + +impl EventEmitter for ProjectPanel {} + +impl EventEmitter for ProjectPanel {} + +impl workspace::dock::Panel for ProjectPanel { + fn position(&self, cx: &WindowContext) -> DockPosition { + match ProjectPanelSettings::get_global(cx).dock { + ProjectPanelDockPosition::Left => DockPosition::Left, + ProjectPanelDockPosition::Right => DockPosition::Right, + } + } + + fn position_is_valid(&self, position: DockPosition) -> bool { + matches!(position, DockPosition::Left | DockPosition::Right) + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings| { + let dock = match position { + DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left, + DockPosition::Right => ProjectPanelDockPosition::Right, + }; + settings.dock = Some(dock); + }, + ); + } + + fn size(&self, cx: &WindowContext) -> f32 { + self.width + .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width) + } + + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + self.width = size; + self.serialize(cx); + cx.notify(); + } + + fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { + Some("icons/project.svg") + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Project Panel".into(), Some(Box::new(ToggleFocus))) + } + + // fn should_change_position_on_event(event: &Self::Event) -> bool { + // matches!(event, Event::DockPositionChanged) + // } + + fn has_focus(&self, _: &WindowContext) -> bool { + self.has_focus + } + + fn persistent_name(&self) -> &'static str { + "Project Panel" + } + + fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle { + self.focus_handle.clone() + } + + // fn is_focus_event(event: &Self::Event) -> bool { + // matches!(event, Event::Focus) + // } +} + +impl ClipboardEntry { + fn is_cut(&self) -> bool { + matches!(self, Self::Cut { .. }) + } + + fn entry_id(&self) -> ProjectEntryId { + match self { + ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => { + *entry_id + } + } + } + + fn worktree_id(&self) -> WorktreeId { + match self { + ClipboardEntry::Copied { worktree_id, .. } + | ClipboardEntry::Cut { worktree_id, .. } => *worktree_id, + } + } +} + +// todo!() +// #[cfg(test)] +// mod tests { +// use super::*; +// use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle}; +// use pretty_assertions::assert_eq; +// use project::FakeFs; +// use serde_json::json; +// use settings::SettingsStore; +// use std::{ +// collections::HashSet, +// path::{Path, PathBuf}, +// sync::atomic::{self, AtomicUsize}, +// }; +// use workspace::{pane, AppState}; + +// #[gpui::test] +// async fn test_visible_list(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.executor().clone()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// toggle_expand_dir(&panel, "root1/b", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b <== selected", +// " > 3", +// " > 4", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// assert_eq!( +// visible_entries_as_strings(&panel, 6..9, cx), +// &[ +// // +// " > C", +// " .dockerignore", +// "v root2", +// ] +// ); +// } + +// #[gpui::test(iterations = 30)] +// async fn test_editing_files(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// select_path(&panel, "root1", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1 <== selected", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// // Add a file with the root folder selected. The filename editor is placed +// // before the first file in the root folder. +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [EDITOR: ''] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); +// panel.confirm(&Confirm, cx).unwrap() +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [PROCESSING: 'the-new-filename'] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// " the-new-filename <== selected", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// select_path(&panel, "root1/b", cx); +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: ''] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel +// .update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx)); +// panel.confirm(&Confirm, cx).unwrap() +// }) +// .await +// .unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " another-filename.txt <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// select_path(&panel, "root1/b/another-filename.txt", cx); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: 'another-filename.txt'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// let file_name_selections = editor.selections.all::(cx); +// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); +// let file_name_selection = &file_name_selections[0]; +// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); +// assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension"); + +// editor.set_text("a-different-filename.tar.gz", cx) +// }); +// panel.confirm(&Confirm, cx).unwrap() +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [PROCESSING: 'a-different-filename.tar.gz'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " a-different-filename.tar.gz <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: 'a-different-filename.tar.gz'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// let file_name_selections = editor.selections.all::(cx); +// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); +// let file_name_selection = &file_name_selections[0]; +// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); +// assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot"); + +// }); +// panel.cancel(&Cancel, cx) +// }); + +// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > [EDITOR: ''] <== selected", +// " > 3", +// " > 4", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("new-dir", cx)); +// panel.confirm(&Confirm, cx).unwrap() +// }); +// panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > [PROCESSING: 'new-dir']", +// " > 3 <== selected", +// " > 4", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3 <== selected", +// " > 4", +// " > new-dir", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > [EDITOR: '3'] <== selected", +// " > 4", +// " > new-dir", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// // Dismiss the rename editor when it loses focus. +// workspace.update(cx, |_, cx| cx.focus_self()); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3 <== selected", +// " > 4", +// " > new-dir", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); +// } + +// #[gpui::test(iterations = 30)] +// async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// select_path(&panel, "root1", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1 <== selected", +// " > .git", +// " > a", +// " > b", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// // Add a file with the root folder selected. The filename editor is placed +// // before the first file in the root folder. +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [EDITOR: ''] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// editor.set_text("/bdir1/dir2/the-new-filename", cx) +// }); +// panel.confirm(&Confirm, cx).unwrap() +// }); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " > C", +// " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..13, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " > b", +// " v bdir1", +// " v dir2", +// " the-new-filename <== selected", +// " > C", +// " .dockerignore", +// "v root2", +// " > d", +// " > e", +// ] +// ); +// } + +// #[gpui::test] +// async fn test_copy_paste(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// "one.two.txt": "", +// "one.txt": "" +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// panel.update(cx, |panel, cx| { +// panel.select_next(&Default::default(), cx); +// panel.select_next(&Default::default(), cx); +// }); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// // +// "v root1", +// " one.two.txt <== selected", +// " one.txt", +// ] +// ); + +// // Regression test - file name is created correctly when +// // the copied file's name contains multiple dots. +// panel.update(cx, |panel, cx| { +// panel.copy(&Default::default(), cx); +// panel.paste(&Default::default(), cx); +// }); +// cx.foreground().run_until_parked(); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// // +// "v root1", +// " one.two copy.txt", +// " one.two.txt <== selected", +// " one.txt", +// ] +// ); + +// panel.update(cx, |panel, cx| { +// panel.paste(&Default::default(), cx); +// }); +// cx.foreground().run_until_parked(); + +// assert_eq!( +// visible_entries_as_strings(&panel, 0..50, cx), +// &[ +// // +// "v root1", +// " one.two copy 1.txt", +// " one.two copy.txt", +// " one.two.txt <== selected", +// " one.txt", +// ] +// ); +// } + +// #[gpui::test] +// async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// toggle_expand_dir(&panel, "src/test", cx); +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ] +// ); +// ensure_single_file_is_opened(window, "test/first.rs", cx); + +// submit_deletion(window.into(), &panel, cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " second.rs", +// " third.rs" +// ], +// "Project panel should have no deleted file, no other file is selected in it" +// ); +// ensure_no_open_items_and_panes(window.into(), &workspace, cx); + +// select_path(&panel, "src/test/second.rs", cx); +// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " second.rs <== selected", +// " third.rs" +// ] +// ); +// ensure_single_file_is_opened(window, "test/second.rs", cx); + +// window.update(cx, |cx| { +// let active_items = workspace +// .read(cx) +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()) +// .collect::>(); +// assert_eq!(active_items.len(), 1); +// let open_editor = active_items +// .into_iter() +// .next() +// .unwrap() +// .downcast::() +// .expect("Open item should be an editor"); +// open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); +// }); +// submit_deletion(window.into(), &panel, cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " v test", " third.rs"], +// "Project panel should have no deleted file, with one last file remaining" +// ); +// ensure_no_open_items_and_panes(window.into(), &workspace, cx); +// } + +// #[gpui::test] +// async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// select_path(&panel, "src/", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src <== selected", " > test"] +// ); +// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " > [EDITOR: ''] <== selected", " > test"] +// ); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("test", cx)); +// assert!( +// panel.confirm(&Confirm, cx).is_none(), +// "Should not allow to confirm on conflicting new directory name" +// ) +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " > test"], +// "File list should be unchanged after failed folder create confirmation" +// ); + +// select_path(&panel, "src/test/", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " > test <== selected"] +// ); +// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " [EDITOR: ''] <== selected", +// " first.rs", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("first.rs", cx)); +// assert!( +// panel.confirm(&Confirm, cx).is_none(), +// "Should not allow to confirm on conflicting new file name" +// ) +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs", +// " second.rs", +// " third.rs" +// ], +// "File list should be unchanged after failed file create confirmation" +// ); + +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ], +// ); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " [EDITOR: 'first.rs'] <== selected", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("second.rs", cx)); +// assert!( +// panel.confirm(&Confirm, cx).is_none(), +// "Should not allow to confirm on conflicting file rename" +// ) +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ], +// "File list should be unchanged after failed rename confirmation" +// ); +// } + +// #[gpui::test] +// async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// let new_search_events_count = Arc::new(AtomicUsize::new(0)); +// let _subscription = panel.update(cx, |_, cx| { +// let subcription_count = Arc::clone(&new_search_events_count); +// cx.subscribe(&cx.handle(), move |_, _, event, _| { +// if matches!(event, Event::NewSearchInDirectory { .. }) { +// subcription_count.fetch_add(1, atomic::Ordering::SeqCst); +// } +// }) +// }); + +// toggle_expand_dir(&panel, "src/test", cx); +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel.new_search_in_directory(&NewSearchInDirectory, cx) +// }); +// assert_eq!( +// new_search_events_count.load(atomic::Ordering::SeqCst), +// 0, +// "Should not trigger new search in directory when called on a file" +// ); + +// select_path(&panel, "src/test", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test <== selected", +// " first.rs", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel.new_search_in_directory(&NewSearchInDirectory, cx) +// }); +// assert_eq!( +// new_search_events_count.load(atomic::Ordering::SeqCst), +// 1, +// "Should trigger new search in directory when called on a directory" +// ); +// } + +// #[gpui::test] +// async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/project_root", +// json!({ +// "dir_1": { +// "nested_dir": { +// "file_a.py": "# File contents", +// "file_b.py": "# File contents", +// "file_c.py": "# File contents", +// }, +// "file_1.py": "# File contents", +// "file_2.py": "# File contents", +// "file_3.py": "# File contents", +// }, +// "dir_2": { +// "file_1.py": "# File contents", +// "file_2.py": "# File contents", +// "file_3.py": "# File contents", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// panel.update(cx, |panel, cx| { +// panel.collapse_all_entries(&CollapseAllEntries, cx) +// }); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v project_root", " > dir_1", " > dir_2",] +// ); + +// // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries +// toggle_expand_dir(&panel, "project_root/dir_1", cx); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v project_root", +// " v dir_1 <== selected", +// " > nested_dir", +// " file_1.py", +// " file_2.py", +// " file_3.py", +// " > dir_2", +// ] +// ); +// } + +// #[gpui::test] +// async fn test_new_file_move(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.as_fake().insert_tree("/root", json!({})).await; +// let project = Project::test(fs, ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// // Make a new buffer with no backing file +// workspace.update(cx, |workspace, cx| { +// Editor::new_file(workspace, &Default::default(), cx) +// }); + +// // "Save as"" the buffer, creating a new backing file for it +// let task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(workspace::SaveIntent::Save, cx) +// }); + +// cx.foreground().run_until_parked(); +// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); +// task.await.unwrap(); + +// // Rename the file +// select_path(&panel, "root/new", cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v root", " new <== selected"] +// ); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("newer", cx)); +// }); +// panel +// .update(cx, |panel, cx| panel.confirm(&Confirm, cx)) +// .unwrap() +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v root", " newer <== selected"] +// ); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.save_active_item(workspace::SaveIntent::Save, cx) +// }) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); +// // assert that saving the file doesn't restore "new" +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v root", " newer <== selected"] +// ); +// } + +// fn toggle_expand_dir( +// panel: &View, +// path: impl AsRef, +// cx: &mut TestAppContext, +// ) { +// let path = path.as_ref(); +// panel.update(cx, |panel, cx| { +// for worktree in panel.project.read(cx).worktrees().collect::>() { +// let worktree = worktree.read(cx); +// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { +// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; +// panel.toggle_expanded(entry_id, cx); +// return; +// } +// } +// panic!("no worktree for path {:?}", path); +// }); +// } + +// fn select_path(panel: &View, path: impl AsRef, cx: &mut TestAppContext) { +// let path = path.as_ref(); +// panel.update(cx, |panel, cx| { +// for worktree in panel.project.read(cx).worktrees().collect::>() { +// let worktree = worktree.read(cx); +// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { +// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; +// panel.selection = Some(Selection { +// worktree_id: worktree.id(), +// entry_id, +// }); +// return; +// } +// } +// panic!("no worktree for path {:?}", path); +// }); +// } + +// fn visible_entries_as_strings( +// panel: &View, +// range: Range, +// cx: &mut TestAppContext, +// ) -> Vec { +// let mut result = Vec::new(); +// let mut project_entries = HashSet::new(); +// let mut has_editor = false; + +// panel.update(cx, |panel, cx| { +// panel.for_each_visible_entry(range, cx, |project_entry, details, _| { +// if details.is_editing { +// assert!(!has_editor, "duplicate editor entry"); +// has_editor = true; +// } else { +// assert!( +// project_entries.insert(project_entry), +// "duplicate project entry {:?} {:?}", +// project_entry, +// details +// ); +// } + +// let indent = " ".repeat(details.depth); +// let icon = if details.kind.is_dir() { +// if details.is_expanded { +// "v " +// } else { +// "> " +// } +// } else { +// " " +// }; +// let name = if details.is_editing { +// format!("[EDITOR: '{}']", details.filename) +// } else if details.is_processing { +// format!("[PROCESSING: '{}']", details.filename) +// } else { +// details.filename.clone() +// }; +// let selected = if details.is_selected { +// " <== selected" +// } else { +// "" +// }; +// result.push(format!("{indent}{icon}{name}{selected}")); +// }); +// }); + +// result +// } + +// fn init_test(cx: &mut TestAppContext) { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// init_settings(cx); +// theme::init(cx); +// language::init(cx); +// editor::init_settings(cx); +// crate::init((), cx); +// workspace::init_settings(cx); +// client::init_settings(cx); +// Project::init_settings(cx); +// }); +// } + +// fn init_test_with_editor(cx: &mut TestAppContext) { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// let app_state = AppState::test(cx); +// theme::init(cx); +// init_settings(cx); +// language::init(cx); +// editor::init(cx); +// pane::init(cx); +// crate::init((), cx); +// workspace::init(app_state.clone(), cx); +// Project::init_settings(cx); +// }); +// } + +// fn ensure_single_file_is_opened( +// window: WindowHandle, +// expected_path: &str, +// cx: &mut TestAppContext, +// ) { +// window.update_root(cx, |workspace, cx| { +// let worktrees = workspace.worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// let worktree_id = WorktreeId::from_usize(worktrees[0].id()); + +// let open_project_paths = workspace +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) +// .collect::>(); +// assert_eq!( +// open_project_paths, +// vec![ProjectPath { +// worktree_id, +// path: Arc::from(Path::new(expected_path)) +// }], +// "Should have opened file, selected in project panel" +// ); +// }); +// } + +// fn submit_deletion( +// window: AnyWindowHandle, +// panel: &View, +// cx: &mut TestAppContext, +// ) { +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts before the deletion" +// ); +// panel.update(cx, |panel, cx| { +// panel +// .delete(&Delete, cx) +// .expect("Deletion start") +// .detach_and_log_err(cx); +// }); +// assert!( +// window.has_pending_prompt(cx), +// "Should have a prompt after the deletion" +// ); +// window.simulate_prompt_answer(0, cx); +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts after prompt was replied to" +// ); +// cx.foreground().run_until_parked(); +// } + +// fn ensure_no_open_items_and_panes( +// window: AnyWindowHandle, +// workspace: &View, +// cx: &mut TestAppContext, +// ) { +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts after deletion operation closes the file" +// ); +// window.read_with(cx, |cx| { +// let open_project_paths = workspace +// .read(cx) +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) +// .collect::>(); +// assert!( +// open_project_paths.is_empty(), +// "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" +// ); +// }); +// } +// } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 053b9d68af..0edc66d0d7 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -1319,53 +1319,56 @@ impl Workspace { // })) // } - // pub fn prepare_to_close( - // &mut self, - // quitting: bool, - // cx: &mut ViewContext, - // ) -> Task> { - // let active_call = self.active_call().cloned(); - // let window = cx.window(); + pub fn prepare_to_close( + &mut self, + quitting: bool, + cx: &mut ViewContext, + ) -> Task> { + //todo!(saveing) + // let active_call = self.active_call().cloned(); + // let window = cx.window(); - // cx.spawn(|this, mut cx| async move { - // let workspace_count = cx - // .windows() - // .into_iter() - // .filter(|window| window.root_is::()) - // .count(); + cx.spawn(|this, mut cx| async move { + // let workspace_count = cx + // .windows() + // .into_iter() + // .filter(|window| window.root_is::()) + // .count(); - // if let Some(active_call) = active_call { - // if !quitting - // && workspace_count == 1 - // && active_call.read_with(&cx, |call, _| call.room().is_some()) - // { - // let answer = window.prompt( - // PromptLevel::Warning, - // "Do you want to leave the current call?", - // &["Close window and hang up", "Cancel"], - // &mut cx, - // ); + // if let Some(active_call) = active_call { + // if !quitting + // && workspace_count == 1 + // && active_call.read_with(&cx, |call, _| call.room().is_some()) + // { + // let answer = window.prompt( + // PromptLevel::Warning, + // "Do you want to leave the current call?", + // &["Close window and hang up", "Cancel"], + // &mut cx, + // ); - // if let Some(mut answer) = answer { - // if answer.next().await == Some(1) { - // return anyhow::Ok(false); - // } else { - // active_call - // .update(&mut cx, |call, cx| call.hang_up(cx)) - // .await - // .log_err(); - // } - // } - // } - // } + // if let Some(mut answer) = answer { + // if answer.next().await == Some(1) { + // return anyhow::Ok(false); + // } else { + // active_call + // .update(&mut cx, |call, cx| call.hang_up(cx)) + // .await + // .log_err(); + // } + // } + // } + // } - // Ok(this - // .update(&mut cx, |this, cx| { - // this.save_all_internal(SaveIntent::Close, cx) - // })? - // .await?) - // }) - // } + Ok( + false, // this + // .update(&mut cx, |this, cx| { + // this.save_all_internal(SaveIntent::Close, cx) + // })? + // .await? + ) + }) + } // fn save_all( // &mut self, diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 20f5d5b1bd..4ae77ccf0c 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -9,6 +9,7 @@ use backtrace::Backtrace; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::UserStore; use db::kvp::KEY_VALUE_STORE; +use editor::Editor; use fs::RealFs; use futures::StreamExt; use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; From d197660d3bef8ccd632ba3c096b020ee1b77bd09 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 13 Nov 2023 16:46:06 -0800 Subject: [PATCH 053/126] Fix broken tests and comment out remaining tests --- .../collab2/src/tests/channel_buffer_tests.rs | 1559 +++---- crates/collab2/src/tests/editor_tests.rs | 3777 +++++++++-------- crates/editor2/src/editor_tests.rs | 1618 +++---- .../src/test/editor_lsp_test_context.rs | 6 +- crates/gpui2/src/elements/text.rs | 8 +- crates/gpui2/src/window.rs | 4 +- 6 files changed, 3494 insertions(+), 3478 deletions(-) diff --git a/crates/collab2/src/tests/channel_buffer_tests.rs b/crates/collab2/src/tests/channel_buffer_tests.rs index 612832672d..63057cbd41 100644 --- a/crates/collab2/src/tests/channel_buffer_tests.rs +++ b/crates/collab2/src/tests/channel_buffer_tests.rs @@ -1,749 +1,379 @@ -use std::ops::Range; +//todo(partially ported) +// use std::ops::Range; -use crate::{ - rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - tests::TestServer, -}; -use client::{Collaborator, ParticipantIndex, UserId}; -use collections::HashMap; -use editor::{Anchor, Editor, ToOffset}; -use futures::future; -use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; -use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; +// use crate::{ +// rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, +// tests::TestServer, +// }; +// use client::{Collaborator, ParticipantIndex, UserId}; +// use collections::HashMap; +// use editor::{Anchor, Editor, ToOffset}; +// use futures::future; +// use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; +// use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; -#[gpui::test] -async fn test_core_channel_buffers( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) - .await; - - // Client A joins the channel buffer - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - // Client A edits the buffer - let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); - buffer_a.update(cx_a, |buffer, cx| { - buffer.edit([(0..0, "hello world")], None, cx) - }); - buffer_a.update(cx_a, |buffer, cx| { - buffer.edit([(5..5, ", cruel")], None, cx) - }); - buffer_a.update(cx_a, |buffer, cx| { - buffer.edit([(0..5, "goodbye")], None, cx) - }); - buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); - assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); - executor.run_until_parked(); - - // Client B joins the channel buffer - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators( - buffer.collaborators(), - &[client_a.user_id(), client_b.user_id()], - ); - }); - - // Client B sees the correct text, and then edits it - let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); - assert_eq!( - buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), - buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) - ); - assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); - buffer_b.update(cx_b, |buffer, cx| { - buffer.edit([(7..12, "beautiful")], None, cx) - }); - - // Both A and B see the new edit - executor.run_until_parked(); - assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); - assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); - - // Client A closes the channel buffer. - cx_a.update(|_| drop(channel_buffer_a)); - executor.run_until_parked(); - - // Client B sees that client A is gone from the channel buffer. - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); - }); - - // Client A rejoins the channel buffer - let _channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - executor.run_until_parked(); - - // Sanity test, make sure we saw A rejoining - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators( - &buffer.collaborators(), - &[client_a.user_id(), client_b.user_id()], - ); - }); - - // Client A loses connection. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - // Client B observes A disconnect - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); - }); - - // TODO: - // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects - // - Test interaction with channel deletion while buffer is open -} - -// todo!("collab_ui") // #[gpui::test] -// async fn test_channel_notes_participant_indices( +// async fn test_core_channel_buffers( // executor: BackgroundExecutor, -// mut cx_a: &mut TestAppContext, -// mut cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// let mut server = TestServer::start(executor.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; - -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); - -// cx_a.update(editor::init); -// cx_b.update(editor::init); -// cx_c.update(editor::init); // let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b), (&client_c, cx_c)], -// ) +// .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) // .await; -// client_a -// .fs() -// .insert_tree("/root", json!({"file.txt": "123"})) -// .await; -// let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; -// let project_b = client_b.build_empty_local_project(cx_b); -// let project_c = client_c.build_empty_local_project(cx_c); -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - -// // Clients A, B, and C open the channel notes -// let channel_view_a = cx_a -// .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) -// .await -// .unwrap(); -// let channel_view_b = cx_b -// .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) -// .await -// .unwrap(); -// let channel_view_c = cx_c -// .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) +// // Client A joins the channel buffer +// let channel_buffer_a = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) // .await // .unwrap(); -// // Clients A, B, and C all insert and select some text -// channel_view_a.update(cx_a, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// editor.insert("a", cx); -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![0..1]); -// }); -// }); +// // Client A edits the buffer +// let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); +// buffer_a.update(cx_a, |buffer, cx| { +// buffer.edit([(0..0, "hello world")], None, cx) // }); -// executor.run_until_parked(); -// channel_view_b.update(cx_b, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// editor.move_down(&Default::default(), cx); -// editor.insert("b", cx); -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![1..2]); -// }); -// }); +// buffer_a.update(cx_a, |buffer, cx| { +// buffer.edit([(5..5, ", cruel")], None, cx) // }); -// executor.run_until_parked(); -// channel_view_c.update(cx_c, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// editor.move_down(&Default::default(), cx); -// editor.insert("c", cx); -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![2..3]); -// }); -// }); -// }); - -// // Client A sees clients B and C without assigned colors, because they aren't -// // in a call together. -// executor.run_until_parked(); -// channel_view_a.update(cx_a, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); -// }); -// }); - -// // Clients A and B join the same call. -// for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { -// call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) -// .await -// .unwrap(); -// } - -// // Clients A and B see each other with two different assigned colors. Client C -// // still doesn't have a color. -// executor.run_until_parked(); -// channel_view_a.update(cx_a, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// assert_remote_selections( -// editor, -// &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], -// cx, -// ); -// }); -// }); -// channel_view_b.update(cx_b, |notes, cx| { -// notes.editor.update(cx, |editor, cx| { -// assert_remote_selections( -// editor, -// &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], -// cx, -// ); -// }); -// }); - -// // Client A shares a project, and client B joins. -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - -// // Clients A and B open the same file. -// let editor_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// editor_a.update(cx_a, |editor, cx| { -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![0..1]); -// }); -// }); -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![2..3]); -// }); +// buffer_a.update(cx_a, |buffer, cx| { +// buffer.edit([(0..5, "goodbye")], None, cx) // }); +// buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); +// assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); // executor.run_until_parked(); -// // Clients A and B see each other with the same colors as in the channel notes. -// editor_a.update(cx_a, |editor, cx| { -// assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); +// // Client B joins the channel buffer +// let channel_buffer_b = client_b +// .channel_store() +// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); +// channel_buffer_b.read_with(cx_b, |buffer, _| { +// assert_collaborators( +// buffer.collaborators(), +// &[client_a.user_id(), client_b.user_id()], +// ); // }); -// editor_b.update(cx_b, |editor, cx| { -// assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); + +// // Client B sees the correct text, and then edits it +// let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); +// assert_eq!( +// buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), +// buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) +// ); +// assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); +// buffer_b.update(cx_b, |buffer, cx| { +// buffer.edit([(7..12, "beautiful")], None, cx) // }); + +// // Both A and B see the new edit +// executor.run_until_parked(); +// assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); +// assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); + +// // Client A closes the channel buffer. +// cx_a.update(|_| drop(channel_buffer_a)); +// executor.run_until_parked(); + +// // Client B sees that client A is gone from the channel buffer. +// channel_buffer_b.read_with(cx_b, |buffer, _| { +// assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); +// }); + +// // Client A rejoins the channel buffer +// let _channel_buffer_a = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); +// executor.run_until_parked(); + +// // Sanity test, make sure we saw A rejoining +// channel_buffer_b.read_with(cx_b, |buffer, _| { +// assert_collaborators( +// &buffer.collaborators(), +// &[client_a.user_id(), client_b.user_id()], +// ); +// }); + +// // Client A loses connection. +// server.forbid_connections(); +// server.disconnect_client(client_a.peer_id().unwrap()); +// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + +// // Client B observes A disconnect +// channel_buffer_b.read_with(cx_b, |buffer, _| { +// assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); +// }); + +// // TODO: +// // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects +// // - Test interaction with channel deletion while buffer is open // } -#[track_caller] -fn assert_remote_selections( - editor: &mut Editor, - expected_selections: &[(Option, Range)], - cx: &mut ViewContext, -) { - let snapshot = editor.snapshot(cx); - let range = Anchor::min()..Anchor::max(); - let remote_selections = snapshot - .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) - .map(|s| { - let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); - let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); - (s.participant_index, start..end) - }) - .collect::>(); - assert_eq!( - remote_selections, expected_selections, - "incorrect remote selections" - ); -} +// // todo!("collab_ui") +// // #[gpui::test] +// // async fn test_channel_notes_participant_indices( +// // executor: BackgroundExecutor, +// // mut cx_a: &mut TestAppContext, +// // mut cx_b: &mut TestAppContext, +// // cx_c: &mut TestAppContext, +// // ) { +// // let mut server = TestServer::start(&executor).await; +// // let client_a = server.create_client(cx_a, "user_a").await; +// // let client_b = server.create_client(cx_b, "user_b").await; +// // let client_c = server.create_client(cx_c, "user_c").await; -#[gpui::test] -async fn test_multiple_handles_to_channel_buffer( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; +// // let active_call_a = cx_a.read(ActiveCall::global); +// // let active_call_b = cx_b.read(ActiveCall::global); - let channel_id = server - .make_channel("the-channel", None, (&client_a, cx_a), &mut []) - .await; +// // cx_a.update(editor::init); +// // cx_b.update(editor::init); +// // cx_c.update(editor::init); - let channel_buffer_1 = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - let channel_buffer_2 = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - let channel_buffer_3 = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); +// // let channel_id = server +// // .make_channel( +// // "the-channel", +// // None, +// // (&client_a, cx_a), +// // &mut [(&client_b, cx_b), (&client_c, cx_c)], +// // ) +// // .await; - // All concurrent tasks for opening a channel buffer return the same model handle. - let (channel_buffer, channel_buffer_2, channel_buffer_3) = - future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) - .await - .unwrap(); - let channel_buffer_model_id = channel_buffer.entity_id(); - assert_eq!(channel_buffer, channel_buffer_2); - assert_eq!(channel_buffer, channel_buffer_3); +// // client_a +// // .fs() +// // .insert_tree("/root", json!({"file.txt": "123"})) +// // .await; +// // let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; +// // let project_b = client_b.build_empty_local_project(cx_b); +// // let project_c = client_c.build_empty_local_project(cx_c); +// // let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// // let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - channel_buffer.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "hello")], None, cx); - }) - }); - deterministic.run_until_parked(); +// // // Clients A, B, and C open the channel notes +// // let channel_view_a = cx_a +// // .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) +// // .await +// // .unwrap(); +// // let channel_view_b = cx_b +// // .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) +// // .await +// // .unwrap(); +// // let channel_view_c = cx_c +// // .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) +// // .await +// // .unwrap(); - cx_a.update(|_| { - drop(channel_buffer); - drop(channel_buffer_2); - drop(channel_buffer_3); - }); - deterministic.run_until_parked(); +// // // Clients A, B, and C all insert and select some text +// // channel_view_a.update(cx_a, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // editor.insert("a", cx); +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![0..1]); +// // }); +// // }); +// // }); +// // executor.run_until_parked(); +// // channel_view_b.update(cx_b, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // editor.move_down(&Default::default(), cx); +// // editor.insert("b", cx); +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![1..2]); +// // }); +// // }); +// // }); +// // executor.run_until_parked(); +// // channel_view_c.update(cx_c, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // editor.move_down(&Default::default(), cx); +// // editor.insert("c", cx); +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![2..3]); +// // }); +// // }); +// // }); - // The channel buffer can be reopened after dropping it. - let channel_buffer = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); - channel_buffer.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, _| { - assert_eq!(buffer.text(), "hello"); - }) - }); -} +// // // Client A sees clients B and C without assigned colors, because they aren't +// // // in a call together. +// // executor.run_until_parked(); +// // channel_view_a.update(cx_a, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); +// // }); +// // }); -#[gpui::test] -async fn test_channel_buffer_disconnect( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; +// // // Clients A and B join the same call. +// // for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { +// // call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) +// // .await +// // .unwrap(); +// // } - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b)], - ) - .await; +// // // Clients A and B see each other with two different assigned colors. Client C +// // // still doesn't have a color. +// // executor.run_until_parked(); +// // channel_view_a.update(cx_a, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // assert_remote_selections( +// // editor, +// // &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], +// // cx, +// // ); +// // }); +// // }); +// // channel_view_b.update(cx_b, |notes, cx| { +// // notes.editor.update(cx, |editor, cx| { +// // assert_remote_selections( +// // editor, +// // &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], +// // cx, +// // ); +// // }); +// // }); - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); +// // // Client A shares a project, and client B joins. +// // let project_id = active_call_a +// // .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// // .await +// // .unwrap(); +// // let project_b = client_b.build_remote_project(project_id, cx_b).await; +// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); +// // // Clients A and B open the same file. +// // let editor_a = workspace_a +// // .update(cx_a, |workspace, cx| { +// // workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) +// // }) +// // .await +// // .unwrap() +// // .downcast::() +// // .unwrap(); +// // let editor_b = workspace_b +// // .update(cx_b, |workspace, cx| { +// // workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) +// // }) +// // .await +// // .unwrap() +// // .downcast::() +// // .unwrap(); - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); +// // editor_a.update(cx_a, |editor, cx| { +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![0..1]); +// // }); +// // }); +// // editor_b.update(cx_b, |editor, cx| { +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![2..3]); +// // }); +// // }); +// // executor.run_until_parked(); - channel_buffer_a.update(cx_a, |buffer, cx| { - assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); - assert!(!buffer.is_connected()); - }); +// // // Clients A and B see each other with the same colors as in the channel notes. +// // editor_a.update(cx_a, |editor, cx| { +// // assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); +// // }); +// // editor_b.update(cx_b, |editor, cx| { +// // assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); +// // }); +// // } - deterministic.run_until_parked(); - - server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - deterministic.run_until_parked(); - - client_a - .channel_store() - .update(cx_a, |channel_store, _| { - channel_store.remove_channel(channel_id) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - - // Channel buffer observed the deletion - channel_buffer_b.update(cx_b, |buffer, cx| { - assert!(buffer.channel(cx).is_none()); - assert!(!buffer.is_connected()); - }); -} - -#[gpui::test] -async fn test_rejoin_channel_buffer( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b)], - ) - .await; - - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - channel_buffer_a.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "1")], None, cx); - }) - }); - deterministic.run_until_parked(); - - // Client A disconnects. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - - // Both clients make an edit. - channel_buffer_a.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(1..1, "2")], None, cx); - }) - }); - channel_buffer_b.update(cx_b, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "0")], None, cx); - }) - }); - - // Both clients see their own edit. - deterministic.run_until_parked(); - channel_buffer_a.read_with(cx_a, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "12"); - }); - channel_buffer_b.read_with(cx_b, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "01"); - }); - - // Client A reconnects. Both clients see each other's edits, and see - // the same collaborators. - server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT); - channel_buffer_a.read_with(cx_a, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - channel_buffer_b.read_with(cx_b, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - - channel_buffer_a.read_with(cx_a, |buffer_a, _| { - channel_buffer_b.read_with(cx_b, |buffer_b, _| { - assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); - }); - }); -} - -#[gpui::test] -async fn test_channel_buffers_and_server_restarts( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - - let channel_id = server - .make_channel( - "the-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b), (&client_c, cx_c)], - ) - .await; - - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let _channel_buffer_c = client_c - .channel_store() - .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - channel_buffer_a.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "1")], None, cx); - }) - }); - deterministic.run_until_parked(); - - // Client C can't reconnect. - client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - - // Server stops. - server.reset().await; - deterministic.advance_clock(RECEIVE_TIMEOUT); - - // While the server is down, both clients make an edit. - channel_buffer_a.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(1..1, "2")], None, cx); - }) - }); - channel_buffer_b.update(cx_b, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "0")], None, cx); - }) - }); - - // Server restarts. - server.start().await.unwrap(); - deterministic.advance_clock(CLEANUP_TIMEOUT); - - // Clients reconnects. Clients A and B see each other's edits, and see - // that client C has disconnected. - channel_buffer_a.read_with(cx_a, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - channel_buffer_b.read_with(cx_b, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - - channel_buffer_a.read_with(cx_a, |buffer_a, _| { - channel_buffer_b.read_with(cx_b, |buffer_b, _| { - assert_collaborators( - buffer_a.collaborators(), - &[client_a.user_id(), client_b.user_id()], - ); - assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); - }); - }); -} - -//todo!(collab_ui) -// #[gpui::test(iterations = 10)] -// async fn test_following_to_channel_notes_without_a_shared_project( -// deterministic: BackgroundExecutor, -// mut cx_a: &mut TestAppContext, -// mut cx_b: &mut TestAppContext, -// mut cx_c: &mut TestAppContext, +// #[track_caller] +// fn assert_remote_selections( +// editor: &mut Editor, +// expected_selections: &[(Option, Range)], +// cx: &mut ViewContext, // ) { -// let mut server = TestServer::start(&deterministic).await; +// let snapshot = editor.snapshot(cx); +// let range = Anchor::min()..Anchor::max(); +// let remote_selections = snapshot +// .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) +// .map(|s| { +// let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); +// let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); +// (s.participant_index, start..end) +// }) +// .collect::>(); +// assert_eq!( +// remote_selections, expected_selections, +// "incorrect remote selections" +// ); +// } + +// #[gpui::test] +// async fn test_multiple_handles_to_channel_buffer( +// deterministic: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(deterministic.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; - -// cx_a.update(editor::init); -// cx_b.update(editor::init); -// cx_c.update(editor::init); -// cx_a.update(collab_ui::channel_view::init); -// cx_b.update(collab_ui::channel_view::init); -// cx_c.update(collab_ui::channel_view::init); - -// let channel_1_id = server -// .make_channel( -// "channel-1", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b), (&client_c, cx_c)], -// ) -// .await; -// let channel_2_id = server -// .make_channel( -// "channel-2", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b), (&client_c, cx_c)], -// ) +// let channel_id = server +// .make_channel("the-channel", None, (&client_a, cx_a), &mut []) // .await; -// // Clients A, B, and C join a channel. -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); -// let active_call_c = cx_c.read(ActiveCall::global); -// for (call, cx) in [ -// (&active_call_a, &mut cx_a), -// (&active_call_b, &mut cx_b), -// (&active_call_c, &mut cx_c), -// ] { -// call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) +// let channel_buffer_1 = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); +// let channel_buffer_2 = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); +// let channel_buffer_3 = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); + +// // All concurrent tasks for opening a channel buffer return the same model handle. +// let (channel_buffer, channel_buffer_2, channel_buffer_3) = +// future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) // .await // .unwrap(); -// } -// deterministic.run_until_parked(); +// let channel_buffer_model_id = channel_buffer.entity_id(); +// assert_eq!(channel_buffer, channel_buffer_2); +// assert_eq!(channel_buffer, channel_buffer_3); -// // Clients A, B, and C all open their own unshared projects. -// client_a.fs().insert_tree("/a", json!({})).await; -// client_b.fs().insert_tree("/b", json!({})).await; -// client_c.fs().insert_tree("/c", json!({})).await; -// let (project_a, _) = client_a.build_local_project("/a", cx_a).await; -// let (project_b, _) = client_b.build_local_project("/b", cx_b).await; -// let (project_c, _) = client_b.build_local_project("/c", cx_c).await; -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); - -// // Client A opens the notes for channel 1. -// let channel_view_1_a = cx_a -// .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) -// .await -// .unwrap(); -// channel_view_1_a.update(cx_a, |notes, cx| { -// assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); -// notes.editor.update(cx, |editor, cx| { -// editor.insert("Hello from A.", cx); -// editor.change_selections(None, cx, |selections| { -// selections.select_ranges(vec![3..4]); -// }); -// }); -// }); - -// // Client B follows client A. -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() +// channel_buffer.update(cx_a, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, cx| { +// buffer.edit([(0..0, "hello")], None, cx); // }) +// }); +// deterministic.run_until_parked(); + +// cx_a.update(|_| { +// drop(channel_buffer); +// drop(channel_buffer_2); +// drop(channel_buffer_3); +// }); +// deterministic.run_until_parked(); + +// // The channel buffer can be reopened after dropping it. +// let channel_buffer = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) // .await // .unwrap(); - -// // Client B is taken to the notes for channel 1, with the same -// // text selected as client A. -// deterministic.run_until_parked(); -// let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.leader_for_pane(workspace.active_pane()), -// Some(client_a.peer_id().unwrap()) -// ); -// workspace -// .active_item(cx) -// .expect("no active item") -// .downcast::() -// .expect("active item is not a channel view") -// }); -// channel_view_1_b.read_with(cx_b, |notes, cx| { -// assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); -// let editor = notes.editor.read(cx); -// assert_eq!(editor.text(cx), "Hello from A."); -// assert_eq!(editor.selections.ranges::(cx), &[3..4]); -// }); - -// // Client A opens the notes for channel 2. -// let channel_view_2_a = cx_a -// .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) -// .await -// .unwrap(); -// channel_view_2_a.read_with(cx_a, |notes, cx| { -// assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); -// }); - -// // Client B is taken to the notes for channel 2. -// deterministic.run_until_parked(); -// let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.leader_for_pane(workspace.active_pane()), -// Some(client_a.peer_id().unwrap()) -// ); -// workspace -// .active_item(cx) -// .expect("no active item") -// .downcast::() -// .expect("active item is not a channel view") -// }); -// channel_view_2_b.read_with(cx_b, |notes, cx| { -// assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); +// assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); +// channel_buffer.update(cx_a, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, _| { +// assert_eq!(buffer.text(), "hello"); +// }) // }); // } -//todo!(collab_ui) // #[gpui::test] -// async fn test_channel_buffer_changes( +// async fn test_channel_buffer_disconnect( // deterministic: BackgroundExecutor, // cx_a: &mut TestAppContext, // cx_b: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&deterministic).await; +// let mut server = TestServer::start(deterministic.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; @@ -762,7 +392,74 @@ async fn test_channel_buffers_and_server_restarts( // .await // .unwrap(); -// // Client A makes an edit, and client B should see that the note has changed. +// let channel_buffer_b = client_b +// .channel_store() +// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); + +// server.forbid_connections(); +// server.disconnect_client(client_a.peer_id().unwrap()); +// deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + +// channel_buffer_a.update(cx_a, |buffer, cx| { +// assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); +// assert!(!buffer.is_connected()); +// }); + +// deterministic.run_until_parked(); + +// server.allow_connections(); +// deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + +// deterministic.run_until_parked(); + +// client_a +// .channel_store() +// .update(cx_a, |channel_store, _| { +// channel_store.remove_channel(channel_id) +// }) +// .await +// .unwrap(); +// deterministic.run_until_parked(); + +// // Channel buffer observed the deletion +// channel_buffer_b.update(cx_b, |buffer, cx| { +// assert!(buffer.channel(cx).is_none()); +// assert!(!buffer.is_connected()); +// }); +// } + +// #[gpui::test] +// async fn test_rejoin_channel_buffer( +// deterministic: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(deterministic.clone()).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; + +// let channel_id = server +// .make_channel( +// "the-channel", +// None, +// (&client_a, cx_a), +// &mut [(&client_b, cx_b)], +// ) +// .await; + +// let channel_buffer_a = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); +// let channel_buffer_b = client_b +// .channel_store() +// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); + // channel_buffer_a.update(cx_a, |buffer, cx| { // buffer.buffer().update(cx, |buffer, cx| { // buffer.edit([(0..0, "1")], None, cx); @@ -770,105 +467,409 @@ async fn test_channel_buffers_and_server_restarts( // }); // deterministic.run_until_parked(); -// let has_buffer_changed = cx_b.update(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_channel_buffer_changed(channel_id) -// .unwrap() -// }); -// assert!(has_buffer_changed); +// // Client A disconnects. +// server.forbid_connections(); +// server.disconnect_client(client_a.peer_id().unwrap()); -// // Opening the buffer should clear the changed flag. -// let project_b = client_b.build_empty_local_project(cx_b); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let channel_view_b = cx_b -// .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) -// .await -// .unwrap(); -// deterministic.run_until_parked(); - -// let has_buffer_changed = cx_b.update(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_channel_buffer_changed(channel_id) -// .unwrap() -// }); -// assert!(!has_buffer_changed); - -// // Editing the channel while the buffer is open should not show that the buffer has changed. +// // Both clients make an edit. // channel_buffer_a.update(cx_a, |buffer, cx| { // buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "2")], None, cx); +// buffer.edit([(1..1, "2")], None, cx); // }) // }); +// channel_buffer_b.update(cx_b, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, cx| { +// buffer.edit([(0..0, "0")], None, cx); +// }) +// }); + +// // Both clients see their own edit. // deterministic.run_until_parked(); - -// let has_buffer_changed = cx_b.read(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_channel_buffer_changed(channel_id) -// .unwrap() +// channel_buffer_a.read_with(cx_a, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "12"); // }); -// assert!(!has_buffer_changed); - -// deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); - -// // Test that the server is tracking things correctly, and we retain our 'not changed' -// // state across a disconnect -// server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic); -// let has_buffer_changed = cx_b.read(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_channel_buffer_changed(channel_id) -// .unwrap() +// channel_buffer_b.read_with(cx_b, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "01"); // }); -// assert!(!has_buffer_changed); -// // Closing the buffer should re-enable change tracking -// cx_b.update(|cx| { -// workspace_b.update(cx, |workspace, cx| { -// workspace.close_all_items_and_panes(&Default::default(), cx) +// // Client A reconnects. Both clients see each other's edits, and see +// // the same collaborators. +// server.allow_connections(); +// deterministic.advance_clock(RECEIVE_TIMEOUT); +// channel_buffer_a.read_with(cx_a, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "012"); +// }); +// channel_buffer_b.read_with(cx_b, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "012"); +// }); + +// channel_buffer_a.read_with(cx_a, |buffer_a, _| { +// channel_buffer_b.read_with(cx_b, |buffer_b, _| { +// assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); // }); - -// drop(channel_view_b) // }); - -// deterministic.run_until_parked(); - -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "3")], None, cx); -// }) -// }); -// deterministic.run_until_parked(); - -// let has_buffer_changed = cx_b.read(|cx| { -// client_b -// .channel_store() -// .read(cx) -// .has_channel_buffer_changed(channel_id) -// .unwrap() -// }); -// assert!(has_buffer_changed); // } -#[track_caller] -fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { - let mut user_ids = collaborators - .values() - .map(|collaborator| collaborator.user_id) - .collect::>(); - user_ids.sort(); - assert_eq!( - user_ids, - ids.into_iter().map(|id| id.unwrap()).collect::>() - ); -} +// #[gpui::test] +// async fn test_channel_buffers_and_server_restarts( +// deterministic: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// cx_c: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(deterministic.clone()).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// let client_c = server.create_client(cx_c, "user_c").await; -fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { - channel_buffer.read_with(cx, |buffer, _| buffer.text()) -} +// let channel_id = server +// .make_channel( +// "the-channel", +// None, +// (&client_a, cx_a), +// &mut [(&client_b, cx_b), (&client_c, cx_c)], +// ) +// .await; + +// let channel_buffer_a = client_a +// .channel_store() +// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); +// let channel_buffer_b = client_b +// .channel_store() +// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); +// let _channel_buffer_c = client_c +// .channel_store() +// .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) +// .await +// .unwrap(); + +// channel_buffer_a.update(cx_a, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, cx| { +// buffer.edit([(0..0, "1")], None, cx); +// }) +// }); +// deterministic.run_until_parked(); + +// // Client C can't reconnect. +// client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); + +// // Server stops. +// server.reset().await; +// deterministic.advance_clock(RECEIVE_TIMEOUT); + +// // While the server is down, both clients make an edit. +// channel_buffer_a.update(cx_a, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, cx| { +// buffer.edit([(1..1, "2")], None, cx); +// }) +// }); +// channel_buffer_b.update(cx_b, |buffer, cx| { +// buffer.buffer().update(cx, |buffer, cx| { +// buffer.edit([(0..0, "0")], None, cx); +// }) +// }); + +// // Server restarts. +// server.start().await.unwrap(); +// deterministic.advance_clock(CLEANUP_TIMEOUT); + +// // Clients reconnects. Clients A and B see each other's edits, and see +// // that client C has disconnected. +// channel_buffer_a.read_with(cx_a, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "012"); +// }); +// channel_buffer_b.read_with(cx_b, |buffer, cx| { +// assert_eq!(buffer.buffer().read(cx).text(), "012"); +// }); + +// channel_buffer_a.read_with(cx_a, |buffer_a, _| { +// channel_buffer_b.read_with(cx_b, |buffer_b, _| { +// assert_collaborators( +// buffer_a.collaborators(), +// &[client_a.user_id(), client_b.user_id()], +// ); +// assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); +// }); +// }); +// } + +// //todo!(collab_ui) +// // #[gpui::test(iterations = 10)] +// // async fn test_following_to_channel_notes_without_a_shared_project( +// // deterministic: BackgroundExecutor, +// // mut cx_a: &mut TestAppContext, +// // mut cx_b: &mut TestAppContext, +// // mut cx_c: &mut TestAppContext, +// // ) { +// // let mut server = TestServer::start(&deterministic).await; +// // let client_a = server.create_client(cx_a, "user_a").await; +// // let client_b = server.create_client(cx_b, "user_b").await; + +// // let client_c = server.create_client(cx_c, "user_c").await; + +// // cx_a.update(editor::init); +// // cx_b.update(editor::init); +// // cx_c.update(editor::init); +// // cx_a.update(collab_ui::channel_view::init); +// // cx_b.update(collab_ui::channel_view::init); +// // cx_c.update(collab_ui::channel_view::init); + +// // let channel_1_id = server +// // .make_channel( +// // "channel-1", +// // None, +// // (&client_a, cx_a), +// // &mut [(&client_b, cx_b), (&client_c, cx_c)], +// // ) +// // .await; +// // let channel_2_id = server +// // .make_channel( +// // "channel-2", +// // None, +// // (&client_a, cx_a), +// // &mut [(&client_b, cx_b), (&client_c, cx_c)], +// // ) +// // .await; + +// // // Clients A, B, and C join a channel. +// // let active_call_a = cx_a.read(ActiveCall::global); +// // let active_call_b = cx_b.read(ActiveCall::global); +// // let active_call_c = cx_c.read(ActiveCall::global); +// // for (call, cx) in [ +// // (&active_call_a, &mut cx_a), +// // (&active_call_b, &mut cx_b), +// // (&active_call_c, &mut cx_c), +// // ] { +// // call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) +// // .await +// // .unwrap(); +// // } +// // deterministic.run_until_parked(); + +// // // Clients A, B, and C all open their own unshared projects. +// // client_a.fs().insert_tree("/a", json!({})).await; +// // client_b.fs().insert_tree("/b", json!({})).await; +// // client_c.fs().insert_tree("/c", json!({})).await; +// // let (project_a, _) = client_a.build_local_project("/a", cx_a).await; +// // let (project_b, _) = client_b.build_local_project("/b", cx_b).await; +// // let (project_c, _) = client_b.build_local_project("/c", cx_c).await; +// // let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// // let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + +// // active_call_a +// // .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) +// // .await +// // .unwrap(); + +// // // Client A opens the notes for channel 1. +// // let channel_view_1_a = cx_a +// // .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) +// // .await +// // .unwrap(); +// // channel_view_1_a.update(cx_a, |notes, cx| { +// // assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); +// // notes.editor.update(cx, |editor, cx| { +// // editor.insert("Hello from A.", cx); +// // editor.change_selections(None, cx, |selections| { +// // selections.select_ranges(vec![3..4]); +// // }); +// // }); +// // }); + +// // // Client B follows client A. +// // workspace_b +// // .update(cx_b, |workspace, cx| { +// // workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() +// // }) +// // .await +// // .unwrap(); + +// // // Client B is taken to the notes for channel 1, with the same +// // // text selected as client A. +// // deterministic.run_until_parked(); +// // let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| { +// // assert_eq!( +// // workspace.leader_for_pane(workspace.active_pane()), +// // Some(client_a.peer_id().unwrap()) +// // ); +// // workspace +// // .active_item(cx) +// // .expect("no active item") +// // .downcast::() +// // .expect("active item is not a channel view") +// // }); +// // channel_view_1_b.read_with(cx_b, |notes, cx| { +// // assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); +// // let editor = notes.editor.read(cx); +// // assert_eq!(editor.text(cx), "Hello from A."); +// // assert_eq!(editor.selections.ranges::(cx), &[3..4]); +// // }); + +// // // Client A opens the notes for channel 2. +// // let channel_view_2_a = cx_a +// // .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) +// // .await +// // .unwrap(); +// // channel_view_2_a.read_with(cx_a, |notes, cx| { +// // assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); +// // }); + +// // // Client B is taken to the notes for channel 2. +// // deterministic.run_until_parked(); +// // let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| { +// // assert_eq!( +// // workspace.leader_for_pane(workspace.active_pane()), +// // Some(client_a.peer_id().unwrap()) +// // ); +// // workspace +// // .active_item(cx) +// // .expect("no active item") +// // .downcast::() +// // .expect("active item is not a channel view") +// // }); +// // channel_view_2_b.read_with(cx_b, |notes, cx| { +// // assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); +// // }); +// // } + +// //todo!(collab_ui) +// // #[gpui::test] +// // async fn test_channel_buffer_changes( +// // deterministic: BackgroundExecutor, +// // cx_a: &mut TestAppContext, +// // cx_b: &mut TestAppContext, +// // ) { +// // let mut server = TestServer::start(&deterministic).await; +// // let client_a = server.create_client(cx_a, "user_a").await; +// // let client_b = server.create_client(cx_b, "user_b").await; + +// // let channel_id = server +// // .make_channel( +// // "the-channel", +// // None, +// // (&client_a, cx_a), +// // &mut [(&client_b, cx_b)], +// // ) +// // .await; + +// // let channel_buffer_a = client_a +// // .channel_store() +// // .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) +// // .await +// // .unwrap(); + +// // // Client A makes an edit, and client B should see that the note has changed. +// // channel_buffer_a.update(cx_a, |buffer, cx| { +// // buffer.buffer().update(cx, |buffer, cx| { +// // buffer.edit([(0..0, "1")], None, cx); +// // }) +// // }); +// // deterministic.run_until_parked(); + +// // let has_buffer_changed = cx_b.update(|cx| { +// // client_b +// // .channel_store() +// // .read(cx) +// // .has_channel_buffer_changed(channel_id) +// // .unwrap() +// // }); +// // assert!(has_buffer_changed); + +// // // Opening the buffer should clear the changed flag. +// // let project_b = client_b.build_empty_local_project(cx_b); +// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// // let channel_view_b = cx_b +// // .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) +// // .await +// // .unwrap(); +// // deterministic.run_until_parked(); + +// // let has_buffer_changed = cx_b.update(|cx| { +// // client_b +// // .channel_store() +// // .read(cx) +// // .has_channel_buffer_changed(channel_id) +// // .unwrap() +// // }); +// // assert!(!has_buffer_changed); + +// // // Editing the channel while the buffer is open should not show that the buffer has changed. +// // channel_buffer_a.update(cx_a, |buffer, cx| { +// // buffer.buffer().update(cx, |buffer, cx| { +// // buffer.edit([(0..0, "2")], None, cx); +// // }) +// // }); +// // deterministic.run_until_parked(); + +// // let has_buffer_changed = cx_b.read(|cx| { +// // client_b +// // .channel_store() +// // .read(cx) +// // .has_channel_buffer_changed(channel_id) +// // .unwrap() +// // }); +// // assert!(!has_buffer_changed); + +// // deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); + +// // // Test that the server is tracking things correctly, and we retain our 'not changed' +// // // state across a disconnect +// // server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic); +// // let has_buffer_changed = cx_b.read(|cx| { +// // client_b +// // .channel_store() +// // .read(cx) +// // .has_channel_buffer_changed(channel_id) +// // .unwrap() +// // }); +// // assert!(!has_buffer_changed); + +// // // Closing the buffer should re-enable change tracking +// // cx_b.update(|cx| { +// // workspace_b.update(cx, |workspace, cx| { +// // workspace.close_all_items_and_panes(&Default::default(), cx) +// // }); + +// // drop(channel_view_b) +// // }); + +// // deterministic.run_until_parked(); + +// // channel_buffer_a.update(cx_a, |buffer, cx| { +// // buffer.buffer().update(cx, |buffer, cx| { +// // buffer.edit([(0..0, "3")], None, cx); +// // }) +// // }); +// // deterministic.run_until_parked(); + +// // let has_buffer_changed = cx_b.read(|cx| { +// // client_b +// // .channel_store() +// // .read(cx) +// // .has_channel_buffer_changed(channel_id) +// // .unwrap() +// // }); +// // assert!(has_buffer_changed); +// // } + +// #[track_caller] +// fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { +// let mut user_ids = collaborators +// .values() +// .map(|collaborator| collaborator.user_id) +// .collect::>(); +// user_ids.sort(); +// assert_eq!( +// user_ids, +// ids.into_iter().map(|id| id.unwrap()).collect::>() +// ); +// } + +// fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { +// channel_buffer.read_with(cx, |buffer, _| buffer.text()) +// } diff --git a/crates/collab2/src/tests/editor_tests.rs b/crates/collab2/src/tests/editor_tests.rs index 8fce187492..07a4269567 100644 --- a/crates/collab2/src/tests/editor_tests.rs +++ b/crates/collab2/src/tests/editor_tests.rs @@ -1,1888 +1,1889 @@ -use std::{ - path::Path, - sync::{ - atomic::{self, AtomicBool, AtomicUsize}, - Arc, - }, -}; - -use call::ActiveCall; -use editor::{ - test::editor_test_context::{AssertionContextManager, EditorTestContext}, - Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, - ToggleCodeActions, Undo, -}; -use gpui::{BackgroundExecutor, TestAppContext, VisualContext, VisualTestContext}; -use indoc::indoc; -use language::{ - language_settings::{AllLanguageSettings, InlayHintSettings}, - tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, -}; -use rpc::RECEIVE_TIMEOUT; -use serde_json::json; -use settings::SettingsStore; -use text::Point; -use workspace::Workspace; - -use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; - -#[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", - serde_json::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().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 workspace_b = - cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); - let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); - - let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "b.txt"), None, true, cx) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - - //TODO: focus - assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx))); - editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); - //todo(is_edited) - // assert!(workspace_b.is_edited(cx_b)); - - // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. - server.forbid_connections(); - 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. - - workspace_b.update(cx_b, |_, cx| { - assert_eq!(cx.focused_view_id(), None); - }); - // assert!(!workspace_b.is_edited(cx_b)); - - // Ensure client B is not prompted to save edits when closing window after disconnecting. - let can_close = workspace_b - .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())); -} - -#[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_empty_window(); - let editor_a = - window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); - let mut editor_cx_a = EditorTestContext { - cx: cx_a, - window: window_a.into(), - editor: editor_a, - assertion_cx: AssertionContextManager::new(), - }; - - // Open a buffer as client B - 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_empty_window(); - let editor_b = - window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); - let mut editor_cx_b = EditorTestContext { - cx: cx_b, - window: window_b.into(), - editor: editor_b, - assertion_cx: AssertionContextManager::new(), - }; - - // Test newline above - 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ˇ - - "}); -} - -#[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_empty_window(); - let editor_b = window_b.build_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.executor().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() }" - ); - }); -} - -#[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"); - }); -} - -#[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;"); - }) -} - -#[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_empty_window(); - let mut server = TestServer::start(executor).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - 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.build_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 - }); -} - -#[gpui::test(iterations = 10)] -async fn test_on_input_format_from_host_to_guest( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(&executor).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { - first_trigger_character: ":".to_string(), - more_trigger_character: Some(vec![">".to_string()]), - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a }", - "other.rs": "// Test file", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - - // Open a file in an editor as the host. - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - let window_a = cx_a.add_empty_window(); - let editor_a = window_a - .update(cx_a, |_, cx| { - cx.build_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)) - }) - .unwrap(); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - executor.run_until_parked(); - - // Receive an OnTypeFormatting request as the host's language server. - // Return some formattings from the host's language server. - fake_language_server.handle_request::( - |params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 14), - ); - - Ok(Some(vec![lsp::TextEdit { - new_text: "~<".to_string(), - range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), - }])) - }, - ); - - // Open the buffer on the guest and see that the formattings worked - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - - // Type a on type formatting trigger character as the guest. - editor_a.update(cx_a, |editor, cx| { - cx.focus(&editor_a); - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(">", cx); - }); - - executor.run_until_parked(); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a>~< }") - }); - - // Undo should remove LSP edits first - editor_a.update(cx_a, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a>~< }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a> }"); - }); - executor.run_until_parked(); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a> }") - }); - - editor_a.update(cx_a, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a> }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a }"); - }); - executor.run_until_parked(); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a }") - }); -} - -#[gpui::test(iterations = 10)] -async fn test_on_input_format_from_guest_to_host( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(&executor).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { - first_trigger_character: ":".to_string(), - more_trigger_character: Some(vec![">".to_string()]), - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a }", - "other.rs": "// Test file", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - - // Open a file in an editor as the guest. - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - let window_b = cx_b.add_empty_window(); - let editor_b = window_b.build_view(cx_b, |cx| { - Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) - }); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - executor.run_until_parked(); - // Type a on type formatting trigger character as the guest. - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(":", cx); - cx.focus(&editor_b); - }); - - // Receive an OnTypeFormatting request as the host's language server. - // Return some formattings from the host's language server. - cx_a.foreground().start_waiting(); - fake_language_server - .handle_request::(|params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 14), - ); - - Ok(Some(vec![lsp::TextEdit { - new_text: "~:".to_string(), - range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), - }])) - }) - .next() - .await - .unwrap(); - cx_a.foreground().finish_waiting(); - - // Open the buffer on the host and see that the formattings worked - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a:~: }") - }); - - // Undo should remove LSP edits first - editor_b.update(cx_b, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a:~: }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a: }"); - }); - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a: }") - }); - - editor_b.update(cx_b, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a: }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a }"); - }); - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a }") - }); -} - -#[gpui::test(iterations = 10)] -async fn test_mutual_editor_inlay_hint_cache_update( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(&executor).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - cx_a.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: false, - show_other_hints: true, - }) - }); - }); - }); - cx_b.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: false, - show_other_hints: true, - }) - }); - }); - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - let language = Arc::new(language); - client_a.language_registry().add(Arc::clone(&language)); - client_b.language_registry().add(language); - - // Client A opens a project. - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", - "other.rs": "// Test file", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - // Client B joins the project - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let workspace_a = client_a.build_workspace(&project_a, cx_a).root_view(cx_a); - cx_a.foreground().start_waiting(); - - // The host opens a rust file. - let _buffer_a = project_a - .update(cx_a, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - let fake_language_server = fake_language_servers.next().await.unwrap(); - let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Set up the language server to return an additional inlay hint on each request. - let edits_made = Arc::new(AtomicUsize::new(0)); - let closure_edits_made = Arc::clone(&edits_made); - fake_language_server - .handle_request::(move |params, _| { - let task_edits_made = Arc::clone(&closure_edits_made); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - let edits_made = task_edits_made.load(atomic::Ordering::Acquire); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, edits_made as u32), - label: lsp::InlayHintLabel::String(edits_made.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await - .unwrap(); - - executor.run_until_parked(); - - let initial_edit = edits_made.load(atomic::Ordering::Acquire); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![initial_edit.to_string()], - extract_hint_labels(editor), - "Host should get its first hints when opens an editor" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Host editor update the cache version after every cache/view change", - ); - }); - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![initial_edit.to_string()], - extract_hint_labels(editor), - "Client should get its first hints when opens an editor" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Guest editor update the cache version after every cache/view change" - ); - }); - - let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); - editor.handle_input(":", cx); - cx.focus(&editor_b); - }); - - executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![after_client_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 2); - }); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![after_client_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 2); - }); - - let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - editor_a.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input("a change to increment both buffers' versions", cx); - cx.focus(&editor_a); - }); - - executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![after_host_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 3); - }); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![after_host_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 3); - }); - - let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - fake_language_server - .request::(()) - .await - .expect("inlay refresh request failed"); - - executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![after_special_edit_for_refresh.to_string()], - extract_hint_labels(editor), - "Host should react to /refresh LSP request" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 4, - "Host should accepted all edits and bump its cache version every time" - ); - }); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![after_special_edit_for_refresh.to_string()], - extract_hint_labels(editor), - "Guest should get a /refresh LSP request propagated by host" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 4, - "Guest should accepted all edits and bump its cache version every time" - ); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_inlay_hint_refresh_is_forwarded( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(&executor).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - cx_a.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: false, - show_type_hints: false, - show_parameter_hints: false, - show_other_hints: false, - }) - }); - }); - }); - cx_b.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - }) - }); - }); - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - let language = Arc::new(language); - client_a.language_registry().add(Arc::clone(&language)); - client_b.language_registry().add(language); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", - "other.rs": "// Test file", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - cx_a.foreground().start_waiting(); - cx_b.foreground().start_waiting(); - - let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - let other_hints = Arc::new(AtomicBool::new(false)); - let fake_language_server = fake_language_servers.next().await.unwrap(); - let closure_other_hints = Arc::clone(&other_hints); - fake_language_server - .handle_request::(move |params, _| { - let task_other_hints = Arc::clone(&closure_other_hints); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - let other_hints = task_other_hints.load(atomic::Ordering::Acquire); - let character = if other_hints { 0 } else { 2 }; - let label = if other_hints { - "other hint" - } else { - "initial hint" - }; - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, character), - label: lsp::InlayHintLabel::String(label.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await - .unwrap(); - cx_a.foreground().finish_waiting(); - cx_b.foreground().finish_waiting(); - - executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert!( - extract_hint_labels(editor).is_empty(), - "Host should get no hints due to them turned off" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 0, - "Turned off hints should not generate version updates" - ); - }); - - executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec!["initial hint".to_string()], - extract_hint_labels(editor), - "Client should get its first hints when opens an editor" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Should update cache verison after first hints" - ); - }); - - other_hints.fetch_or(true, atomic::Ordering::Release); - fake_language_server - .request::(()) - .await - .expect("inlay refresh request failed"); - executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert!( - extract_hint_labels(editor).is_empty(), - "Host should get nop hints due to them turned off, even after the /refresh" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 0, - "Turned off hints should not generate version updates, again" - ); - }); - - executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec!["other hint".to_string()], - extract_hint_labels(editor), - "Guest should get a /refresh LSP request propagated by host despite host hints are off" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 2, - "Guest should accepted all edits and bump its cache version every time" - ); - }); -} - -fn extract_hint_labels(editor: &Editor) -> Vec { - let mut labels = Vec::new(); - for hint in editor.inlay_hint_cache().hints() { - match hint.label { - project::InlayHintLabel::String(s) => labels.push(s), - _ => unreachable!(), - } - } - labels -} +//todo(partially ported) +// use std::{ +// path::Path, +// sync::{ +// atomic::{self, AtomicBool, AtomicUsize}, +// Arc, +// }, +// }; + +// use call::ActiveCall; +// use editor::{ +// test::editor_test_context::{AssertionContextManager, EditorTestContext}, +// Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, +// ToggleCodeActions, Undo, +// }; +// use gpui::{BackgroundExecutor, TestAppContext, VisualContext, VisualTestContext}; +// use indoc::indoc; +// use language::{ +// language_settings::{AllLanguageSettings, InlayHintSettings}, +// tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, +// }; +// use rpc::RECEIVE_TIMEOUT; +// use serde_json::json; +// use settings::SettingsStore; +// use text::Point; +// use workspace::Workspace; + +// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; + +// #[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", +// serde_json::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().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 workspace_b = +// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); +// let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); + +// let editor_b = workspace_b +// .update(cx_b, |workspace, cx| { +// workspace.open_path((worktree_id, "b.txt"), None, true, cx) +// }) +// .unwrap() +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// //TODO: focus +// assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx))); +// editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); +// //todo(is_edited) +// // assert!(workspace_b.is_edited(cx_b)); + +// // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. +// server.forbid_connections(); +// 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. + +// workspace_b.update(cx_b, |_, cx| { +// assert_eq!(cx.focused_view_id(), None); +// }); +// // assert!(!workspace_b.is_edited(cx_b)); + +// // Ensure client B is not prompted to save edits when closing window after disconnecting. +// let can_close = workspace_b +// .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())); +// } + +// #[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_empty_window(); +// let editor_a = +// window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); +// let mut editor_cx_a = EditorTestContext { +// cx: cx_a, +// window: window_a.into(), +// editor: editor_a, +// assertion_cx: AssertionContextManager::new(), +// }; + +// // Open a buffer as client B +// 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_empty_window(); +// let editor_b = +// window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); +// let mut editor_cx_b = EditorTestContext { +// cx: cx_b, +// window: window_b.into(), +// editor: editor_b, +// assertion_cx: AssertionContextManager::new(), +// }; + +// // Test newline above +// 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ˇ + +// "}); +// } + +// #[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_empty_window(); +// let editor_b = window_b.build_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.executor().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() }" +// ); +// }); +// } + +// #[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"); +// }); +// } + +// #[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;"); +// }) +// } + +// #[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_empty_window(); +// let mut server = TestServer::start(executor).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// let client_c = server.create_client(cx_c, "user_c").await; +// 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.build_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 +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_on_input_format_from_host_to_guest( +// executor: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(&executor).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// server +// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) +// .await; +// let active_call_a = cx_a.read(ActiveCall::global); + +// // Set up a fake language server. +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_language_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { +// first_trigger_character: ":".to_string(), +// more_trigger_character: Some(vec![">".to_string()]), +// }), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; +// client_a.language_registry().add(Arc::new(language)); + +// client_a +// .fs() +// .insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { a }", +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; +// let project_id = active_call_a +// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// .await +// .unwrap(); +// let project_b = client_b.build_remote_project(project_id, cx_b).await; + +// // Open a file in an editor as the host. +// let buffer_a = project_a +// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) +// .await +// .unwrap(); +// let window_a = cx_a.add_empty_window(); +// let editor_a = window_a +// .update(cx_a, |_, cx| { +// cx.build_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)) +// }) +// .unwrap(); + +// let fake_language_server = fake_language_servers.next().await.unwrap(); +// executor.run_until_parked(); + +// // Receive an OnTypeFormatting request as the host's language server. +// // Return some formattings from the host's language server. +// fake_language_server.handle_request::( +// |params, _| async move { +// assert_eq!( +// params.text_document_position.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// assert_eq!( +// params.text_document_position.position, +// lsp::Position::new(0, 14), +// ); + +// Ok(Some(vec![lsp::TextEdit { +// new_text: "~<".to_string(), +// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), +// }])) +// }, +// ); + +// // Open the buffer on the guest and see that the formattings worked +// let buffer_b = project_b +// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) +// .await +// .unwrap(); + +// // Type a on type formatting trigger character as the guest. +// editor_a.update(cx_a, |editor, cx| { +// cx.focus(&editor_a); +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input(">", cx); +// }); + +// executor.run_until_parked(); + +// buffer_b.read_with(cx_b, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a>~< }") +// }); + +// // Undo should remove LSP edits first +// editor_a.update(cx_a, |editor, cx| { +// assert_eq!(editor.text(cx), "fn main() { a>~< }"); +// editor.undo(&Undo, cx); +// assert_eq!(editor.text(cx), "fn main() { a> }"); +// }); +// executor.run_until_parked(); + +// buffer_b.read_with(cx_b, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a> }") +// }); + +// editor_a.update(cx_a, |editor, cx| { +// assert_eq!(editor.text(cx), "fn main() { a> }"); +// editor.undo(&Undo, cx); +// assert_eq!(editor.text(cx), "fn main() { a }"); +// }); +// executor.run_until_parked(); + +// buffer_b.read_with(cx_b, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a }") +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_on_input_format_from_guest_to_host( +// executor: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(&executor).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// server +// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) +// .await; +// let active_call_a = cx_a.read(ActiveCall::global); + +// // Set up a fake language server. +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_language_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { +// first_trigger_character: ":".to_string(), +// more_trigger_character: Some(vec![">".to_string()]), +// }), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; +// client_a.language_registry().add(Arc::new(language)); + +// client_a +// .fs() +// .insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { a }", +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; +// let project_id = active_call_a +// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// .await +// .unwrap(); +// let project_b = client_b.build_remote_project(project_id, cx_b).await; + +// // Open a file in an editor as the guest. +// let buffer_b = project_b +// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) +// .await +// .unwrap(); +// let window_b = cx_b.add_empty_window(); +// let editor_b = window_b.build_view(cx_b, |cx| { +// Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) +// }); + +// let fake_language_server = fake_language_servers.next().await.unwrap(); +// executor.run_until_parked(); +// // Type a on type formatting trigger character as the guest. +// editor_b.update(cx_b, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input(":", cx); +// cx.focus(&editor_b); +// }); + +// // Receive an OnTypeFormatting request as the host's language server. +// // Return some formattings from the host's language server. +// cx_a.foreground().start_waiting(); +// fake_language_server +// .handle_request::(|params, _| async move { +// assert_eq!( +// params.text_document_position.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// assert_eq!( +// params.text_document_position.position, +// lsp::Position::new(0, 14), +// ); + +// Ok(Some(vec![lsp::TextEdit { +// new_text: "~:".to_string(), +// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), +// }])) +// }) +// .next() +// .await +// .unwrap(); +// cx_a.foreground().finish_waiting(); + +// // Open the buffer on the host and see that the formattings worked +// let buffer_a = project_a +// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) +// .await +// .unwrap(); +// executor.run_until_parked(); + +// buffer_a.read_with(cx_a, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a:~: }") +// }); + +// // Undo should remove LSP edits first +// editor_b.update(cx_b, |editor, cx| { +// assert_eq!(editor.text(cx), "fn main() { a:~: }"); +// editor.undo(&Undo, cx); +// assert_eq!(editor.text(cx), "fn main() { a: }"); +// }); +// executor.run_until_parked(); + +// buffer_a.read_with(cx_a, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a: }") +// }); + +// editor_b.update(cx_b, |editor, cx| { +// assert_eq!(editor.text(cx), "fn main() { a: }"); +// editor.undo(&Undo, cx); +// assert_eq!(editor.text(cx), "fn main() { a }"); +// }); +// executor.run_until_parked(); + +// buffer_a.read_with(cx_a, |buffer, _| { +// assert_eq!(buffer.text(), "fn main() { a }") +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_mutual_editor_inlay_hint_cache_update( +// executor: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(&executor).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// server +// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) +// .await; +// let active_call_a = cx_a.read(ActiveCall::global); +// let active_call_b = cx_b.read(ActiveCall::global); + +// cx_a.update(editor::init); +// cx_b.update(editor::init); + +// cx_a.update(|cx| { +// cx.update_global(|store: &mut SettingsStore, cx| { +// store.update_user_settings::(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: true, +// show_parameter_hints: false, +// show_other_hints: true, +// }) +// }); +// }); +// }); +// cx_b.update(|cx| { +// cx.update_global(|store: &mut SettingsStore, cx| { +// store.update_user_settings::(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: true, +// show_parameter_hints: false, +// show_other_hints: true, +// }) +// }); +// }); +// }); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_language_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// inlay_hint_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; +// let language = Arc::new(language); +// client_a.language_registry().add(Arc::clone(&language)); +// client_b.language_registry().add(language); + +// // Client A opens a project. +// client_a +// .fs() +// .insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; +// active_call_a +// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) +// .await +// .unwrap(); +// let project_id = active_call_a +// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// .await +// .unwrap(); + +// // Client B joins the project +// let project_b = client_b.build_remote_project(project_id, cx_b).await; +// active_call_b +// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) +// .await +// .unwrap(); + +// let workspace_a = client_a.build_workspace(&project_a, cx_a).root_view(cx_a); +// cx_a.foreground().start_waiting(); + +// // The host opens a rust file. +// let _buffer_a = project_a +// .update(cx_a, |project, cx| { +// project.open_local_buffer("/a/main.rs", cx) +// }) +// .await +// .unwrap(); +// let fake_language_server = fake_language_servers.next().await.unwrap(); +// let editor_a = workspace_a +// .update(cx_a, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// // Set up the language server to return an additional inlay hint on each request. +// let edits_made = Arc::new(AtomicUsize::new(0)); +// let closure_edits_made = Arc::clone(&edits_made); +// fake_language_server +// .handle_request::(move |params, _| { +// let task_edits_made = Arc::clone(&closure_edits_made); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// let edits_made = task_edits_made.load(atomic::Ordering::Acquire); +// Ok(Some(vec![lsp::InlayHint { +// position: lsp::Position::new(0, edits_made as u32), +// label: lsp::InlayHintLabel::String(edits_made.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await +// .unwrap(); + +// executor.run_until_parked(); + +// let initial_edit = edits_made.load(atomic::Ordering::Acquire); +// editor_a.update(cx_a, |editor, _| { +// assert_eq!( +// vec![initial_edit.to_string()], +// extract_hint_labels(editor), +// "Host should get its first hints when opens an editor" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 1, +// "Host editor update the cache version after every cache/view change", +// ); +// }); +// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// let editor_b = workspace_b +// .update(cx_b, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// executor.run_until_parked(); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec![initial_edit.to_string()], +// extract_hint_labels(editor), +// "Client should get its first hints when opens an editor" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 1, +// "Guest editor update the cache version after every cache/view change" +// ); +// }); + +// let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; +// editor_b.update(cx_b, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); +// editor.handle_input(":", cx); +// cx.focus(&editor_b); +// }); + +// executor.run_until_parked(); +// editor_a.update(cx_a, |editor, _| { +// assert_eq!( +// vec![after_client_edit.to_string()], +// extract_hint_labels(editor), +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!(inlay_cache.version(), 2); +// }); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec![after_client_edit.to_string()], +// extract_hint_labels(editor), +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!(inlay_cache.version(), 2); +// }); + +// let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; +// editor_a.update(cx_a, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input("a change to increment both buffers' versions", cx); +// cx.focus(&editor_a); +// }); + +// executor.run_until_parked(); +// editor_a.update(cx_a, |editor, _| { +// assert_eq!( +// vec![after_host_edit.to_string()], +// extract_hint_labels(editor), +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!(inlay_cache.version(), 3); +// }); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec![after_host_edit.to_string()], +// extract_hint_labels(editor), +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!(inlay_cache.version(), 3); +// }); + +// let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; +// fake_language_server +// .request::(()) +// .await +// .expect("inlay refresh request failed"); + +// executor.run_until_parked(); +// editor_a.update(cx_a, |editor, _| { +// assert_eq!( +// vec![after_special_edit_for_refresh.to_string()], +// extract_hint_labels(editor), +// "Host should react to /refresh LSP request" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 4, +// "Host should accepted all edits and bump its cache version every time" +// ); +// }); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec![after_special_edit_for_refresh.to_string()], +// extract_hint_labels(editor), +// "Guest should get a /refresh LSP request propagated by host" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 4, +// "Guest should accepted all edits and bump its cache version every time" +// ); +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_inlay_hint_refresh_is_forwarded( +// executor: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(&executor).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// server +// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) +// .await; +// let active_call_a = cx_a.read(ActiveCall::global); +// let active_call_b = cx_b.read(ActiveCall::global); + +// cx_a.update(editor::init); +// cx_b.update(editor::init); + +// cx_a.update(|cx| { +// cx.update_global(|store: &mut SettingsStore, cx| { +// store.update_user_settings::(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: false, +// show_type_hints: false, +// show_parameter_hints: false, +// show_other_hints: false, +// }) +// }); +// }); +// }); +// cx_b.update(|cx| { +// cx.update_global(|store: &mut SettingsStore, cx| { +// store.update_user_settings::(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: true, +// show_parameter_hints: true, +// show_other_hints: true, +// }) +// }); +// }); +// }); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_language_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// inlay_hint_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; +// let language = Arc::new(language); +// client_a.language_registry().add(Arc::clone(&language)); +// client_b.language_registry().add(language); + +// client_a +// .fs() +// .insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; +// active_call_a +// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) +// .await +// .unwrap(); +// let project_id = active_call_a +// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// .await +// .unwrap(); + +// let project_b = client_b.build_remote_project(project_id, cx_b).await; +// active_call_b +// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) +// .await +// .unwrap(); + +// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// cx_a.foreground().start_waiting(); +// cx_b.foreground().start_waiting(); + +// let editor_a = workspace_a +// .update(cx_a, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// let editor_b = workspace_b +// .update(cx_b, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// let other_hints = Arc::new(AtomicBool::new(false)); +// let fake_language_server = fake_language_servers.next().await.unwrap(); +// let closure_other_hints = Arc::clone(&other_hints); +// fake_language_server +// .handle_request::(move |params, _| { +// let task_other_hints = Arc::clone(&closure_other_hints); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// let other_hints = task_other_hints.load(atomic::Ordering::Acquire); +// let character = if other_hints { 0 } else { 2 }; +// let label = if other_hints { +// "other hint" +// } else { +// "initial hint" +// }; +// Ok(Some(vec![lsp::InlayHint { +// position: lsp::Position::new(0, character), +// label: lsp::InlayHintLabel::String(label.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await +// .unwrap(); +// cx_a.foreground().finish_waiting(); +// cx_b.foreground().finish_waiting(); + +// executor.run_until_parked(); +// editor_a.update(cx_a, |editor, _| { +// assert!( +// extract_hint_labels(editor).is_empty(), +// "Host should get no hints due to them turned off" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 0, +// "Turned off hints should not generate version updates" +// ); +// }); + +// executor.run_until_parked(); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec!["initial hint".to_string()], +// extract_hint_labels(editor), +// "Client should get its first hints when opens an editor" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 1, +// "Should update cache verison after first hints" +// ); +// }); + +// other_hints.fetch_or(true, atomic::Ordering::Release); +// fake_language_server +// .request::(()) +// .await +// .expect("inlay refresh request failed"); +// executor.run_until_parked(); +// editor_a.update(cx_a, |editor, _| { +// assert!( +// extract_hint_labels(editor).is_empty(), +// "Host should get nop hints due to them turned off, even after the /refresh" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 0, +// "Turned off hints should not generate version updates, again" +// ); +// }); + +// executor.run_until_parked(); +// editor_b.update(cx_b, |editor, _| { +// assert_eq!( +// vec!["other hint".to_string()], +// extract_hint_labels(editor), +// "Guest should get a /refresh LSP request propagated by host despite host hints are off" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.version(), +// 2, +// "Guest should accepted all edits and bump its cache version every time" +// ); +// }); +// } + +// fn extract_hint_labels(editor: &Editor) -> Vec { +// let mut labels = Vec::new(); +// for hint in editor.inlay_hint_cache().hints() { +// match hint.label { +// project::InlayHintLabel::String(s) => labels.push(s), +// _ => unreachable!(), +// } +// } +// labels +// } diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 0243d5a0b0..9798735bf6 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -36,120 +36,121 @@ use workspace::{ NavigationEntry, ViewId, }; -#[gpui::test] -fn test_edit_events(cx: &mut TestAppContext) { - init_test(cx, |_| {}); +// todo(finish edit tests) +// #[gpui::test] +// fn test_edit_events(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); - let buffer = cx.build_model(|cx| { - let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456"); - buffer.set_group_interval(Duration::from_secs(1)); - buffer - }); +// let buffer = cx.build_model(|cx| { +// let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456"); +// buffer.set_group_interval(Duration::from_secs(1)); +// buffer +// }); - let events = Rc::new(RefCell::new(Vec::new())); - let editor1 = cx.add_window({ - let events = events.clone(); - |cx| { - let view = cx.view().clone(); - cx.subscribe(&view, move |_, _, event, _| { - if matches!(event, Event::Edited | Event::BufferEdited) { - events.borrow_mut().push(("editor1", event.clone())); - } - }) - .detach(); - Editor::for_buffer(buffer.clone(), None, cx) - } - }); +// let events = Rc::new(RefCell::new(Vec::new())); +// let editor1 = cx.add_window({ +// let events = events.clone(); +// |cx| { +// let view = cx.view().clone(); +// cx.subscribe(&view, move |_, _, event, _| { +// if matches!(event, Event::Edited | Event::BufferEdited) { +// events.borrow_mut().push(("editor1", event.clone())); +// } +// }) +// .detach(); +// Editor::for_buffer(buffer.clone(), None, cx) +// } +// }); - let editor2 = cx.add_window({ - let events = events.clone(); - |cx| { - cx.subscribe(&cx.view().clone(), move |_, _, event, _| { - if matches!(event, Event::Edited | Event::BufferEdited) { - events.borrow_mut().push(("editor2", event.clone())); - } - }) - .detach(); - Editor::for_buffer(buffer.clone(), None, cx) - } - }); +// let editor2 = cx.add_window({ +// let events = events.clone(); +// |cx| { +// cx.subscribe(&cx.view().clone(), move |_, _, event, _| { +// if matches!(event, Event::Edited | Event::BufferEdited) { +// events.borrow_mut().push(("editor2", event.clone())); +// } +// }) +// .detach(); +// Editor::for_buffer(buffer.clone(), None, cx) +// } +// }); - assert_eq!(mem::take(&mut *events.borrow_mut()), []); +// assert_eq!(mem::take(&mut *events.borrow_mut()), []); - // Mutating editor 1 will emit an `Edited` event only for that editor. - editor1.update(cx, |editor, cx| editor.insert("X", cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor1", Event::Edited), - ("editor1", Event::BufferEdited), - ("editor2", Event::BufferEdited), - ] - ); +// // Mutating editor 1 will emit an `Edited` event only for that editor. +// editor1.update(cx, |editor, cx| editor.insert("X", cx)); +// assert_eq!( +// mem::take(&mut *events.borrow_mut()), +// [ +// ("editor1", Event::Edited), +// ("editor1", Event::BufferEdited), +// ("editor2", Event::BufferEdited), +// ] +// ); - // Mutating editor 2 will emit an `Edited` event only for that editor. - editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor2", Event::Edited), - ("editor1", Event::BufferEdited), - ("editor2", Event::BufferEdited), - ] - ); +// // 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), - ] - ); +// // Undoing on editor 1 will emit an `Edited` event only for that editor. +// editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); +// assert_eq!( +// mem::take(&mut *events.borrow_mut()), +// [ +// ("editor1", Event::Edited), +// ("editor1", Event::BufferEdited), +// ("editor2", Event::BufferEdited), +// ] +// ); - // Redoing on editor 1 will emit an `Edited` event only for that editor. - editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor1", Event::Edited), - ("editor1", Event::BufferEdited), - ("editor2", Event::BufferEdited), - ] - ); +// // Redoing on editor 1 will emit an `Edited` event only for that editor. +// editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); +// assert_eq!( +// mem::take(&mut *events.borrow_mut()), +// [ +// ("editor1", Event::Edited), +// ("editor1", Event::BufferEdited), +// ("editor2", Event::BufferEdited), +// ] +// ); - // Undoing on editor 2 will emit an `Edited` event only for that editor. - editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor2", Event::Edited), - ("editor1", Event::BufferEdited), - ("editor2", Event::BufferEdited), - ] - ); +// // Undoing on editor 2 will emit an `Edited` event only for that editor. +// editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); +// assert_eq!( +// mem::take(&mut *events.borrow_mut()), +// [ +// ("editor2", Event::Edited), +// ("editor1", Event::BufferEdited), +// ("editor2", Event::BufferEdited), +// ] +// ); - // Redoing on editor 2 will emit an `Edited` event only for that editor. - editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor2", Event::Edited), - ("editor1", Event::BufferEdited), - ("editor2", Event::BufferEdited), - ] - ); +// // Redoing on editor 2 will emit an `Edited` event only for that editor. +// editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); +// assert_eq!( +// mem::take(&mut *events.borrow_mut()), +// [ +// ("editor2", Event::Edited), +// ("editor1", Event::BufferEdited), +// ("editor2", Event::BufferEdited), +// ] +// ); - // No event is emitted when the mutation is a no-op. - editor2.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([0..0])); +// // 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()), []); -} +// editor.backspace(&Backspace, cx); +// }); +// assert_eq!(mem::take(&mut *events.borrow_mut()), []); +// } #[gpui::test] fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { @@ -513,123 +514,124 @@ fn test_clone(cx: &mut TestAppContext) { ); } -#[gpui::test] -async fn test_navigation_history(cx: &mut TestAppContext) { - init_test(cx, |_| {}); +//todo!(editor navigate) +// #[gpui::test] +// async fn test_navigation_history(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); - use workspace::item::Item; +// use workspace::item::Item; - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); - let pane = workspace - .update(cx, |workspace, _| workspace.active_pane().clone()) - .unwrap(); +// let fs = FakeFs::new(cx.executor()); +// let project = Project::test(fs, [], cx).await; +// let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let pane = workspace +// .update(cx, |workspace, _| workspace.active_pane().clone()) +// .unwrap(); - workspace.update(cx, |v, cx| { - cx.build_view(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); - let mut editor = build_editor(buffer.clone(), cx); - let handle = cx.view(); - editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); +// workspace.update(cx, |v, cx| { +// cx.build_view(|cx| { +// let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); +// let mut editor = build_editor(buffer.clone(), cx); +// let handle = cx.view(); +// editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); - fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { - editor.nav_history.as_mut().unwrap().pop_backward(cx) - } +// 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 small distance. +// // Nothing is added to the navigation history. +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) +// }); +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) +// }); +// assert!(pop_history(&mut editor, cx).is_none()); - // Move the cursor a large distance. - // The history can jump back to the previous position. - editor.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)]) - }); - let nav_entry = pop_history(&mut editor, cx).unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item.id(), cx.entity_id()); - assert_eq!( - editor.selections.display_ranges(cx), - &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] - ); - assert!(pop_history(&mut editor, cx).is_none()); +// // Move the cursor a large distance. +// // The history can jump back to the previous position. +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)]) +// }); +// let nav_entry = pop_history(&mut editor, cx).unwrap(); +// editor.navigate(nav_entry.data.unwrap(), cx); +// assert_eq!(nav_entry.item.id(), cx.entity_id()); +// assert_eq!( +// editor.selections.display_ranges(cx), +// &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] +// ); +// assert!(pop_history(&mut editor, cx).is_none()); - // Move the cursor a small distance via the mouse. - // Nothing is added to the navigation history. - editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); - editor.end_selection(cx); - assert_eq!( - editor.selections.display_ranges(cx), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] - ); - assert!(pop_history(&mut editor, cx).is_none()); +// // Move the cursor a small distance via the mouse. +// // Nothing is added to the navigation history. +// editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); +// editor.end_selection(cx); +// assert_eq!( +// editor.selections.display_ranges(cx), +// &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] +// ); +// assert!(pop_history(&mut editor, cx).is_none()); - // Move the cursor a large distance via the mouse. - // The history can jump back to the previous position. - editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); - editor.end_selection(cx); - assert_eq!( - editor.selections.display_ranges(cx), - &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] - ); - let nav_entry = pop_history(&mut editor, cx).unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item.id(), cx.entity_id()); - assert_eq!( - editor.selections.display_ranges(cx), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] - ); - assert!(pop_history(&mut editor, cx).is_none()); +// // Move the cursor a large distance via the mouse. +// // The history can jump back to the previous position. +// editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); +// editor.end_selection(cx); +// assert_eq!( +// editor.selections.display_ranges(cx), +// &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] +// ); +// let nav_entry = pop_history(&mut editor, cx).unwrap(); +// editor.navigate(nav_entry.data.unwrap(), cx); +// assert_eq!(nav_entry.item.id(), cx.entity_id()); +// assert_eq!( +// editor.selections.display_ranges(cx), +// &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] +// ); +// assert!(pop_history(&mut editor, cx).is_none()); - // Set scroll position to check later - editor.set_scroll_position(gpui::Point::::new(5.5, 5.5), cx); - let original_scroll_position = editor.scroll_manager.anchor(); +// // Set scroll position to check later +// editor.set_scroll_position(gpui::Point::::new(5.5, 5.5), cx); +// let original_scroll_position = editor.scroll_manager.anchor(); - // Jump to the end of the document and adjust scroll - editor.move_to_end(&MoveToEnd, cx); - editor.set_scroll_position(gpui::Point::::new(-2.5, -0.5), cx); - assert_ne!(editor.scroll_manager.anchor(), original_scroll_position); +// // Jump to the end of the document and adjust scroll +// editor.move_to_end(&MoveToEnd, cx); +// editor.set_scroll_position(gpui::Point::::new(-2.5, -0.5), cx); +// assert_ne!(editor.scroll_manager.anchor(), original_scroll_position); - let nav_entry = pop_history(&mut editor, cx).unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); +// let nav_entry = pop_history(&mut editor, cx).unwrap(); +// editor.navigate(nav_entry.data.unwrap(), cx); +// assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); - // Ensure we don't panic when navigation data contains invalid anchors *and* points. - let mut invalid_anchor = editor.scroll_manager.anchor().anchor; - invalid_anchor.text_anchor.buffer_id = Some(999); - let invalid_point = Point::new(9999, 0); - editor.navigate( - Box::new(NavigationData { - cursor_anchor: invalid_anchor, - cursor_position: invalid_point, - scroll_anchor: ScrollAnchor { - anchor: invalid_anchor, - offset: Default::default(), - }, - scroll_top_row: invalid_point.row, - }), - cx, - ); - assert_eq!( - editor.selections.display_ranges(cx), - &[editor.max_point(cx)..editor.max_point(cx)] - ); - assert_eq!( - editor.scroll_position(cx), - gpui::Point::new(0., editor.max_point(cx).row() as f32) - ); +// // Ensure we don't panic when navigation data contains invalid anchors *and* points. +// let mut invalid_anchor = editor.scroll_manager.anchor().anchor; +// invalid_anchor.text_anchor.buffer_id = Some(999); +// let invalid_point = Point::new(9999, 0); +// editor.navigate( +// Box::new(NavigationData { +// cursor_anchor: invalid_anchor, +// cursor_position: invalid_point, +// scroll_anchor: ScrollAnchor { +// anchor: invalid_anchor, +// offset: Default::default(), +// }, +// scroll_top_row: invalid_point.row, +// }), +// cx, +// ); +// assert_eq!( +// editor.selections.display_ranges(cx), +// &[editor.max_point(cx)..editor.max_point(cx)] +// ); +// assert_eq!( +// editor.scroll_position(cx), +// gpui::Point::new(0., editor.max_point(cx).row() as f32) +// ); - editor - }) - }); -} +// editor +// }) +// }); +// } #[gpui::test] fn test_cancel(cx: &mut TestAppContext) { @@ -956,55 +958,56 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { }); } -#[gpui::test] -fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { - init_test(cx, |_| {}); +//todo!(finish editor tests) +// #[gpui::test] +// fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); - build_editor(buffer.clone(), cx) - }); - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); - }); - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "abcd".len())] - ); +// let view = cx.add_window(|cx| { +// let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); +// build_editor(buffer.clone(), cx) +// }); +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); +// }); +// view.move_down(&MoveDown, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(1, "abcd".len())] +// ); - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "αβγ".len())] - ); +// view.move_down(&MoveDown, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(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(3, "abcd".len())] +// ); - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(4, "ⓐⓑⓒⓓⓔ".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(3, "abcd".len())] +// ); - view.move_up(&MoveUp, 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(2, "αβγ".len())] +// ); +// }); +// } #[gpui::test] fn test_beginning_end_of_line(cx: &mut TestAppContext) { @@ -1221,63 +1224,64 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { }); } -#[gpui::test] -fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { - init_test(cx, |_| {}); +//todo!(finish editor tests) +// #[gpui::test] +// fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); - build_editor(buffer, cx) - }); +// let view = cx.add_window(|cx| { +// let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); +// build_editor(buffer, cx) +// }); - view.update(cx, |view, cx| { - view.set_wrap_width(Some(140.0.into()), cx); - assert_eq!( - view.display_text(cx), - "use one::{\n two::three::\n four::five\n};" - ); +// view.update(cx, |view, cx| { +// view.set_wrap_width(Some(140.0.into()), cx); +// assert_eq!( +// view.display_text(cx), +// "use one::{\n two::three::\n four::five\n};" +// ); - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]); - }); +// view.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, 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(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, 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_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(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)] - ); - }); -} +// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] +// ); +// }); +// } //todo!(simulate_resize) // #[gpui::test] @@ -2488,136 +2492,137 @@ fn test_delete_line(cx: &mut TestAppContext) { }); } -#[gpui::test] -fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { - init_test(cx, |_| {}); +//todo!(select_anchor_ranges) +// #[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(); +// 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)] - ); +// 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 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)] - ); +// // 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)] - ); +// // 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)] - ); +// // 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 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)] - ); +// // 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, - ) - }); +// // 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 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 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"); +// // We ignore any leading tabs +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); - editor - }); -} +// editor +// }); +// } -#[gpui::test] -fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { - init_test(cx, |_| {}); +// #[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(); +// 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.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"); +// 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 - }); -} +// 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) { @@ -3055,99 +3060,100 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { }); } -#[gpui::test] -fn test_transpose(cx: &mut TestAppContext) { - init_test(cx, |_| {}); +//todo!(test_transpose) +// #[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); +// _ = 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.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), "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.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "bac"); +// assert_eq!(editor.selections.ranges(cx), [3..3]); - editor - }); +// editor +// }); - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); +// _ = 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([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.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), "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.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "acbd\ne"); +// assert_eq!(editor.selections.ranges(cx), [6..6]); - editor - }); +// editor +// }); - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); +// _ = 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.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), "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), "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), "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.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "bcaed\n"); +// assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); - editor - }); +// editor +// }); - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); +// _ = cx.add_window(|cx| { +// let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); - editor.change_selections(None, cx, |s| s.select_ranges([4..4])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "🏀🍐✋"); - assert_eq!(editor.selections.ranges(cx), [8..8]); +// editor.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.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 - }); -} +// editor +// }); +// } //todo!(clipboard) // #[gpui::test] @@ -4805,114 +4811,115 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { }); } -#[gpui::test] -async fn test_snippets(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); +// todo!(select_anchor_ranges) +// #[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 (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, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - let cx = &mut cx; +// let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); +// let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); +// let cx = &mut cx; - editor.update(cx, |editor, cx| { - let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); +// 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(); +// 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); - } +// 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 - "}, - ); +// 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 - "}, - ); +// // 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 - "}, - ); +// 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 - "}, - ); +// 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 - "}, - ); +// 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 - "}, - ); - }); -} +// // 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) { @@ -6328,254 +6335,255 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { }); } -#[gpui::test] -fn test_highlighted_ranges(cx: &mut TestAppContext) { - init_test(cx, |_| {}); +//todo!(finish editor tests) +// #[gpui::test] +// fn test_highlighted_ranges(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); - let editor = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - build_editor(buffer.clone(), cx) - }); +// let editor = cx.add_window(|cx| { +// let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); +// build_editor(buffer.clone(), cx) +// }); - editor.update(cx, |editor, cx| { - struct Type1; - struct Type2; +// editor.update(cx, |editor, cx| { +// struct Type1; +// struct Type2; - let buffer = editor.buffer.read(cx).snapshot(cx); +// let buffer = editor.buffer.read(cx).snapshot(cx); - let anchor_range = - |range: Range| buffer.anchor_after(range.start)..buffer.anchor_after(range.end); +// 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, - ); +// 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, - cx.theme().colors(), - ); - // Enforce a consistent ordering based on color without relying on the ordering of the - // highlight's `TypeId` which is non-executor. - 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, - cx.theme().colors(), - ), - &[( - DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Hsla::red(), - )] - ); - }); -} +// 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, +// cx.theme().colors(), +// ); +// // Enforce a consistent ordering based on color without relying on the ordering of the +// // highlight's `TypeId` which is non-executor. +// 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, +// cx.theme().colors(), +// ), +// &[( +// DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), +// Hsla::red(), +// )] +// ); +// }); +// } -#[gpui::test] -async fn test_following(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); +// todo!(following) +// #[gpui::test] +// async fn test_following(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; +// let fs = FakeFs::new(cx.executor()); +// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let buffer = project.update(cx, |project, cx| { - let buffer = project - .create_buffer(&sample_text(16, 8, 'a'), None, cx) - .unwrap(); - cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)) - }); - let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx)); - let follower = cx.update(|cx| { - cx.open_window( - WindowOptions { - bounds: WindowBounds::Fixed(Bounds::from_corners( - gpui::Point::new((0. as f64).into(), (0. as f64).into()), - gpui::Point::new((10. as f64).into(), (80. as f64).into()), - )), - ..Default::default() - }, - |cx| cx.build_view(|cx| build_editor(buffer.clone(), cx)), - ) - }); +// let buffer = project.update(cx, |project, cx| { +// let buffer = project +// .create_buffer(&sample_text(16, 8, 'a'), None, cx) +// .unwrap(); +// cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)) +// }); +// let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx)); +// let follower = cx.update(|cx| { +// cx.open_window( +// WindowOptions { +// bounds: WindowBounds::Fixed(Bounds::from_corners( +// gpui::Point::new((0. as f64).into(), (0. as f64).into()), +// gpui::Point::new((10. as f64).into(), (80. as f64).into()), +// )), +// ..Default::default() +// }, +// |cx| cx.build_view(|cx| build_editor(buffer.clone(), cx)), +// ) +// }); - let 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.root_view(cx).unwrap(), - move |_, leader, event, cx| { - leader - .read(cx) - .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); - }, - ) - .detach(); +// 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.root_view(cx).unwrap(), +// move |_, leader, event, cx| { +// leader +// .read(cx) +// .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); +// }, +// ) +// .detach(); - cx.subscribe( - &follower.root_view(cx).unwrap(), - move |_, _, event: &Event, cx| { - if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { - *is_still_following.borrow_mut() = false; - } +// cx.subscribe( +// &follower.root_view(cx).unwrap(), +// move |_, _, event: &Event, cx| { +// if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { +// *is_still_following.borrow_mut() = false; +// } - if let Event::BufferEdited = event { - *follower_edit_event_count.borrow_mut() += 1; - } - }, - ) - .detach(); - } - }); +// 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) - }) - .unwrap() - .await - .unwrap(); - follower.update(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 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) +// }) +// .unwrap() +// .await +// .unwrap(); +// follower.update(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(gpui::Point::new(1.5, 3.5), cx); - }); - follower - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - follower - .update(cx, |follower, cx| follower.scroll_position(cx)) - .unwrap(), - gpui::Point::new(1.5, 3.5) - ); - 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(gpui::Point::new(1.5, 3.5), cx); +// }); +// follower +// .update(cx, |follower, cx| { +// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) +// }) +// .unwrap() +// .await +// .unwrap(); +// assert_eq!( +// follower +// .update(cx, |follower, cx| follower.scroll_position(cx)) +// .unwrap(), +// gpui::Point::new(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(gpui::Point::new(1.5, 3.5), cx); - }); - follower - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) - }) - .unwrap() - .await - .unwrap(); - follower.update(cx, |follower, cx| { - assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0)); - assert_eq!(follower.selections.ranges(cx), vec![0..0]); - }); - assert_eq!(*is_still_following.borrow(), true); +// // 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(gpui::Point::new(1.5, 3.5), cx); +// }); +// follower +// .update(cx, |follower, cx| { +// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) +// }) +// .unwrap() +// .await +// .unwrap(); +// follower.update(cx, |follower, cx| { +// assert_eq!(follower.scroll_position(cx), gpui::Point::new(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) - }) - .unwrap() - .await - .unwrap(); - follower.update(cx, |follower, cx| { - assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); - }); - 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) +// }) +// .unwrap() +// .await +// .unwrap(); +// follower.update(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) - }) - .unwrap() - .await - .unwrap(); - follower.update(cx, |follower, cx| { - assert_eq!(follower.selections.ranges(cx), vec![0..2]); - }); +// // 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) +// }) +// .unwrap() +// .await +// .unwrap(); +// follower.update(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: gpui::Point::new(0.0, 0.5), - }, - cx, - ); - }); - assert_eq!(*is_still_following.borrow(), false); -} +// // 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: gpui::Point::new(0.0, 0.5), +// }, +// cx, +// ); +// }); +// assert_eq!(*is_still_following.borrow(), false); +// } -//todo!(following) // #[gpui::test] // async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { // init_test(cx, |_| {}); diff --git a/crates/editor2/src/test/editor_lsp_test_context.rs b/crates/editor2/src/test/editor_lsp_test_context.rs index afcefad6b2..7ee55cddba 100644 --- a/crates/editor2/src/test/editor_lsp_test_context.rs +++ b/crates/editor2/src/test/editor_lsp_test_context.rs @@ -59,6 +59,7 @@ impl<'a> EditorLspTestContext<'a> { .await; let project = Project::test(app_state.fs.clone(), [], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); app_state @@ -68,7 +69,9 @@ impl<'a> EditorLspTestContext<'a> { .await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root_view(cx).unwrap(); + let mut cx = VisualTestContext::from_window(*window.deref(), cx); project .update(&mut cx, |project, cx| { @@ -78,7 +81,6 @@ impl<'a> EditorLspTestContext<'a> { .unwrap(); cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) .await; - let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let item = workspace .update(&mut cx, |workspace, cx| { @@ -86,7 +88,6 @@ impl<'a> EditorLspTestContext<'a> { }) .await .expect("Could not open test file"); - let editor = cx.update(|cx| { item.act_as::(cx) .expect("Opened test file wasn't an editor") @@ -94,7 +95,6 @@ impl<'a> EditorLspTestContext<'a> { editor.update(&mut cx, |editor, cx| editor.focus(cx)); let lsp = fake_servers.next().await.unwrap(); - Self { cx: EditorTestContext { cx, diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 5c5709d32e..eee0584460 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -81,6 +81,7 @@ impl Element for Text { let text = self.text.clone(); let rem_size = cx.rem_size(); + let layout_id = cx.request_measured_layout(Default::default(), rem_size, { let element_state = element_state.clone(); move |known_dimensions, _| { @@ -93,6 +94,10 @@ impl Element for Text { ) .log_err() else { + element_state.lock().replace(TextElementState { + lines: Default::default(), + line_height, + }); return Size::default(); }; @@ -131,7 +136,8 @@ impl Element for Text { let element_state = element_state.lock(); let element_state = element_state .as_ref() - .expect("measurement has not been performed"); + .ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text)) + .unwrap(); let line_height = element_state.line_height; let mut line_origin = bounds.origin; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index fbc9cff1b5..425fb9510f 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2138,7 +2138,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { &mut self, handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, ) { - let handle = self.view(); + let handle = self.view().clone(); self.window_cx.on_key_event(move |event, phase, cx| { handle.update(cx, |view, cx| { handler(view, event, phase, cx); @@ -2151,7 +2151,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { action_type: TypeId, handler: impl Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext) + 'static, ) { - let handle = self.view(); + let handle = self.view().clone(); self.window_cx .on_action(action_type, move |action, phase, cx| { handle.update(cx, |view, cx| { From 38888696db1ba85964b8f1e058075ac724c98eaa Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 19:46:15 -0500 Subject: [PATCH 054/126] Allow a button to take a color --- crates/ui2/src/components/button.rs | 35 +++++++++++-------- crates/ui2/src/components/icon.rs | 44 ++++++++++++++++++++---- crates/ui2/src/components/icon_button.rs | 6 ++-- crates/ui2/src/components/label.rs | 36 ++++++++++++------- crates/workspace2/src/workspace2.rs | 26 +++++++++++--- 5 files changed, 105 insertions(+), 42 deletions(-) diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 5787616832..1418a977f1 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -87,6 +87,7 @@ pub struct Button { label: SharedString, variant: ButtonVariant, width: Option, + color: Option, } impl Button { @@ -99,6 +100,7 @@ impl Button { label: label.into(), variant: Default::default(), width: Default::default(), + color: None, } } @@ -139,25 +141,24 @@ impl Button { self } - fn label_color(&self) -> LabelColor { + pub fn color(mut self, color: Option) -> Self { + self.color = color; + self + } + + pub fn label_color(&self, color: Option) -> LabelColor { if self.disabled { LabelColor::Disabled + } else if let Some(color) = color { + color } else { Default::default() } } - fn icon_color(&self) -> IconColor { - if self.disabled { - IconColor::Disabled - } else { - Default::default() - } - } - - fn render_label(&self) -> Label { + fn render_label(&self, color: LabelColor) -> Label { Label::new(self.label.clone()) - .color(self.label_color()) + .color(color) .line_height_style(LineHeightStyle::UILabel) } @@ -166,7 +167,11 @@ impl Button { } pub fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let icon_color = self.icon_color(); + let (icon_color, label_color) = match (self.disabled, self.color) { + (true, _) => (IconColor::Disabled, LabelColor::Disabled), + (_, None) => (IconColor::Default, LabelColor::Default), + (_, Some(color)) => (IconColor::from(color), color), + }; let mut button = h_stack() .id(SharedString::from(format!("{}", self.label))) @@ -182,16 +187,16 @@ impl Button { (Some(_), Some(IconPosition::Left)) => { button = button .gap_1() - .child(self.render_label()) + .child(self.render_label(label_color)) .children(self.render_icon(icon_color)) } (Some(_), Some(IconPosition::Right)) => { button = button .gap_1() .children(self.render_icon(icon_color)) - .child(self.render_label()) + .child(self.render_label(label_color)) } - (_, _) => button = button.child(self.render_label()), + (_, _) => button = button.child(self.render_label(label_color)), } if let Some(width) = self.width { diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 907f3f9187..75c8129608 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -1,7 +1,7 @@ use gpui::{rems, svg, Hsla}; use strum::EnumIter; -use crate::prelude::*; +use crate::{prelude::*, LabelColor}; #[derive(Default, PartialEq, Copy, Clone)] pub enum IconSize { @@ -14,15 +14,20 @@ pub enum IconSize { pub enum IconColor { #[default] Default, - Muted, - Disabled, - Placeholder, Accent, + Created, + Deleted, + Disabled, Error, - Warning, - Success, + Hidden, Info, + Modified, + Muted, + Placeholder, + Player(u32), Selected, + Success, + Warning, } impl IconColor { @@ -38,6 +43,33 @@ impl IconColor { IconColor::Success => cx.theme().status().success, IconColor::Info => cx.theme().status().info, IconColor::Selected => cx.theme().colors().icon_accent, + IconColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, + IconColor::Created => cx.theme().status().created, + IconColor::Modified => cx.theme().status().modified, + IconColor::Deleted => cx.theme().status().deleted, + IconColor::Hidden => cx.theme().status().hidden, + } + } +} + +impl From for IconColor { + fn from(label: LabelColor) -> Self { + match label { + LabelColor::Default => IconColor::Default, + LabelColor::Muted => IconColor::Muted, + LabelColor::Disabled => IconColor::Disabled, + LabelColor::Placeholder => IconColor::Placeholder, + LabelColor::Accent => IconColor::Accent, + LabelColor::Error => IconColor::Error, + LabelColor::Warning => IconColor::Warning, + LabelColor::Success => IconColor::Success, + LabelColor::Info => IconColor::Info, + LabelColor::Selected => IconColor::Selected, + LabelColor::Player(i) => IconColor::Player(i), + LabelColor::Created => IconColor::Created, + LabelColor::Modified => IconColor::Modified, + LabelColor::Deleted => IconColor::Deleted, + LabelColor::Hidden => IconColor::Hidden, } } } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 91653ea8cd..b648683a8b 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use gpui::{rems, MouseButton}; +use gpui::MouseButton; use crate::{h_stack, prelude::*}; use crate::{ClickHandler, Icon, IconColor, IconElement}; @@ -88,9 +88,7 @@ impl IconButton { .id(self.id.clone()) .justify_center() .rounded_md() - // todo!("Where do these numbers come from?") - .py(rems(0.21875)) - .px(rems(0.375)) + .p_1() .bg(bg_color) .hover(|style| style.bg(bg_hover_color)) .active(|style| style.bg(bg_active_color)) diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 827ba87918..d1d2e0cc9d 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -8,28 +8,40 @@ use crate::styled_ext::StyledExt; pub enum LabelColor { #[default] Default, - Muted, + Accent, Created, - Modified, Deleted, Disabled, + Error, Hidden, + Info, + Modified, + Muted, Placeholder, - Accent, + Player(u32), + Selected, + Success, + Warning, } impl LabelColor { pub fn hsla(&self, cx: &WindowContext) -> Hsla { match self { - Self::Default => cx.theme().colors().text, - Self::Muted => cx.theme().colors().text_muted, - Self::Created => cx.theme().status().created, - Self::Modified => cx.theme().status().modified, - Self::Deleted => cx.theme().status().deleted, - Self::Disabled => cx.theme().colors().text_disabled, - Self::Hidden => cx.theme().status().hidden, - Self::Placeholder => cx.theme().colors().text_placeholder, - Self::Accent => cx.theme().colors().text_accent, + LabelColor::Default => cx.theme().colors().text, + LabelColor::Muted => cx.theme().colors().text_muted, + LabelColor::Created => cx.theme().status().created, + LabelColor::Modified => cx.theme().status().modified, + LabelColor::Deleted => cx.theme().status().deleted, + LabelColor::Disabled => cx.theme().colors().text_disabled, + LabelColor::Hidden => cx.theme().status().hidden, + LabelColor::Info => cx.theme().status().info, + LabelColor::Placeholder => cx.theme().colors().text_placeholder, + LabelColor::Accent => cx.theme().colors().text_accent, + LabelColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, + LabelColor::Error => cx.theme().status().error, + LabelColor::Selected => cx.theme().colors().text_accent, + LabelColor::Success => cx.theme().status().success, + LabelColor::Warning => cx.theme().status().warning, } } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 14a7685a9b..d55d38209a 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -69,7 +69,7 @@ use std::{ }; use theme2::ActiveTheme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use ui::{h_stack, Label}; +use ui::{h_stack, Button, ButtonVariant, Label, LabelColor}; use util::ResultExt; use uuid::Uuid; use workspace_settings::{AutosaveSetting, WorkspaceSettings}; @@ -2641,19 +2641,35 @@ impl Workspace { h_stack() .id("titlebar") .justify_between() - .w_full() - .h(rems(1.75)) - .bg(cx.theme().colors().title_bar_background) .when( !matches!(cx.window_bounds(), WindowBounds::Fullscreen), |s| s.pl_20(), ) + .w_full() + .h(rems(1.75)) + .bg(cx.theme().colors().title_bar_background) .on_click(|_, event, cx| { if event.up.click_count == 2 { cx.zoom_window(); } }) - .child(h_stack().child(Label::new("Left side titlebar item"))) // self.titlebar_item + .child( + h_stack() + // TODO - Add player menu + .child( + Button::new("player") + .variant(ButtonVariant::Ghost) + .color(Some(LabelColor::Player(0))), + ) + // TODO - Add project menu + .child(Button::new("project_name").variant(ButtonVariant::Ghost)) + // TODO - Add git menu + .child( + Button::new("branch_name") + .variant(ButtonVariant::Ghost) + .color(Some(LabelColor::Muted)), + ), + ) // self.titlebar_item .child(h_stack().child(Label::new("Right side titlebar item"))) } From 8bbced50c27b8079f67400e8be217721fabd81c2 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 13 Nov 2023 16:49:17 -0800 Subject: [PATCH 055/126] Add test tag --- crates/gpui2/src/util.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/gpui2/src/util.rs b/crates/gpui2/src/util.rs index 3ee80306ea..cba7ed84b5 100644 --- a/crates/gpui2/src/util.rs +++ b/crates/gpui2/src/util.rs @@ -1,9 +1,15 @@ +#[cfg(any(test, feature = "test-support"))] use std::time::Duration; +#[cfg(any(test, feature = "test-support"))] use futures::Future; + +#[cfg(any(test, feature = "test-support"))] use smol::future::FutureExt; + pub use util::*; +#[cfg(any(test, feature = "test-support"))] pub async fn timeout(timeout: Duration, f: F) -> Result where F: Future, From 76754c559ccfad91c8c1bb785462c637a780cc56 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 18:18:25 -0700 Subject: [PATCH 056/126] WIP --- crates/editor2/src/element.rs | 9 ++ crates/gpui2/src/element.rs | 53 +++++++ crates/gpui2/src/elements/div.rs | 12 +- crates/gpui2/src/elements/img.rs | 9 ++ crates/gpui2/src/elements/node.rs | 172 +++++++++++++++++++--- crates/gpui2/src/elements/svg.rs | 9 ++ crates/gpui2/src/elements/text.rs | 9 ++ crates/gpui2/src/elements/uniform_list.rs | 12 +- crates/gpui2/src/interactive.rs | 2 +- crates/gpui2/src/view.rs | 29 ++++ crates/ui2/src/components/modal.rs | 2 +- 11 files changed, 296 insertions(+), 22 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index f8386ee271..38ec0f3d11 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2640,6 +2640,15 @@ impl Element for EditorElement { cx.request_layout(&style, None) } + fn prepaint( + &mut self, + bounds: Bounds, + view_state: &mut Editor, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + } + fn paint( &mut self, bounds: Bounds, diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 9ee9eaa7c3..1dc9c155eb 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -26,6 +26,14 @@ pub trait Element { cx: &mut ViewContext, ) -> LayoutId; + fn prepaint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ); + fn paint( &mut self, bounds: Bounds, @@ -62,6 +70,7 @@ pub trait ParentElement { trait ElementObject { fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext); fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId; + fn prepaint(&mut self, view_state: &mut V, cx: &mut ViewContext); fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); fn measure( &mut self, @@ -199,6 +208,36 @@ where }; } + fn prepaint(&mut self, view_state: &mut V, cx: &mut ViewContext) { + self.phase = match mem::take(&mut self.phase) { + ElementRenderPhase::LayoutRequested { + layout_id, + mut frame_state, + } + | ElementRenderPhase::LayoutComputed { + layout_id, + mut frame_state, + .. + } => { + let bounds = cx.layout_bounds(layout_id); + if let Some(id) = self.element.id() { + cx.with_element_state(id, |element_state, cx| { + let mut element_state = element_state.unwrap(); + self.element + .prepaint(bounds, view_state, &mut element_state, cx); + ((), element_state) + }); + } else { + self.element + .prepaint(bounds, view_state, frame_state.as_mut().unwrap(), cx); + } + ElementRenderPhase::Painted + } + + _ => panic!("must call layout before paint"), + }; + } + fn measure( &mut self, available_space: Size, @@ -283,6 +322,10 @@ impl AnyElement { self.0.paint(view_state, cx) } + pub fn prepaint(&mut self, view_state: &mut V, cx: &mut ViewContext) { + self.0.prepaint(view_state, cx) + } + /// Initializes this element and performs layout within the given available space to determine its size. pub fn measure( &mut self, @@ -376,6 +419,16 @@ where rendered_element.layout(view_state, cx) } + fn prepaint( + &mut self, + _bounds: Bounds, + view_state: &mut V, + rendered_element: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + rendered_element.prepaint(view_state, cx) + } + fn paint( &mut self, _bounds: Bounds, diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 080cdc2c4d..1211a6bc12 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -265,6 +265,16 @@ where }) } + fn prepaint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + todo!() + } + fn paint( &mut self, bounds: Bounds, @@ -309,7 +319,7 @@ where cx.with_z_index(0, |cx| { style.paint(bounds, cx); this.key_dispatch.paint(bounds, cx); - this.interactivity.paint( + this.interactivity.handle_events( bounds, content_size, style.overflow, diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 1ff088c1af..137dde7dfe 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -94,6 +94,15 @@ where self.base.layout(view_state, element_state, cx) } + fn prepaint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + } + fn paint( &mut self, bounds: Bounds, diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index ec73af8721..bcf3772aed 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -10,6 +10,7 @@ use smallvec::SmallVec; use std::{ any::{Any, TypeId}, marker::PhantomData, + mem, sync::Arc, }; @@ -530,24 +531,6 @@ pub struct Node { children: Vec>, } -pub struct Interactivity { - group: Option, - pub dispatch_context: KeyContext, - pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, - pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, - pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, - pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, - pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, - pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, - pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, - pub hover_style: StyleRefinement, - pub group_hover_style: Option, - drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, - group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, - drop_listeners: SmallVec<[(TypeId, Box>); 2]>, - scroll_offset: Point, -} - impl Node { fn compute_style(&self) -> Style { let mut style = Style::default(); @@ -610,6 +593,20 @@ impl Element for Node { }) } + fn prepaint( + &mut self, + bounds: Bounds, + view_state: &mut V, + _: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + for child in &mut self.children { + child.prepaint(view_state, cx); + } + self.interactivity + .refine_style(&mut self.style, bounds, view_state, cx); + } + fn paint( &mut self, bounds: Bounds, @@ -652,6 +649,7 @@ impl Element for Node { cx.with_z_index(z_index, |cx| { cx.with_z_index(0, |cx| { style.paint(bounds, cx); + self.interactivity.paint(bounds, cx); }); cx.with_z_index(1, |cx| { style.with_text_style(cx, |cx| { @@ -673,6 +671,144 @@ impl Element for Node { } } +pub struct Interactivity { + pub hover_style: StyleRefinement, + pub group_hover_style: Option, + pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, + pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, + group: Option, + pub dispatch_context: KeyContext, + pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, + pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, + pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, + pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, + pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, + pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, + pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, + drop_listeners: SmallVec<[(TypeId, Box>); 2]>, + scroll_offset: Point, +} + +impl Interactivity { + fn refine_style( + &self, + style: &mut StyleRefinement, + bounds: Bounds, + cx: &mut ViewContext, + ) { + let mouse_position = cx.mouse_position(); + if let Some(group_hover) = self.group_hover_style.as_ref() { + if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { + if group_bounds.contains_point(&mouse_position) { + style.refine(&group_hover.style); + } + } + } + if bounds.contains_point(&mouse_position) { + style.refine(&self.hover_style); + } + + if let Some(drag) = cx.active_drag.take() { + for (state_type, group_drag_style) in &self.group_drag_over_styles { + if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { + if *state_type == drag.view.entity_type() + && group_bounds.contains_point(&mouse_position) + { + style.refine(&group_drag_style.style); + } + } + } + + for (state_type, drag_over_style) in &self.drag_over_styles { + if *state_type == drag.view.entity_type() && bounds.contains_point(&mouse_position) + { + style.refine(drag_over_style); + } + } + + cx.active_drag = Some(drag); + } + } + + fn paint(&mut self, bounds: Bounds, cx: &mut ViewContext) { + for listener in self.mouse_down_listeners.drain(..) { + cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { + listener(state, event, &bounds, phase, cx); + }) + } + + for listener in self.mouse_up_listeners.drain(..) { + cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| { + listener(state, event, &bounds, phase, cx); + }) + } + + for listener in self.mouse_move_listeners.drain(..) { + cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| { + listener(state, event, &bounds, phase, cx); + }) + } + + for listener in self.scroll_wheel_listeners.drain(..) { + cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| { + listener(state, event, &bounds, phase, cx); + }) + } + + let hover_group_bounds = self + .group_hover_style + .as_ref() + .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx)); + + if let Some(group_bounds) = hover_group_bounds { + let hovered = group_bounds.contains_point(&cx.mouse_position()); + cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture { + if group_bounds.contains_point(&event.position) != hovered { + cx.notify(); + } + } + }); + } + + if self.hover_style.is_some() + || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty()) + { + let hovered = bounds.contains_point(&cx.mouse_position()); + cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture { + if bounds.contains_point(&event.position) != hovered { + cx.notify(); + } + } + }); + } + + if cx.active_drag.is_some() { + let drop_listeners = mem::take(&mut self.drop_listeners); + cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if let Some(drag_state_type) = + cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) + { + for (drop_state_type, listener) in &drop_listeners { + if *drop_state_type == drag_state_type { + let drag = cx + .active_drag + .take() + .expect("checked for type drag state type above"); + listener(view, drag.view.clone(), cx); + cx.notify(); + cx.stop_propagation(); + } + } + } + } + }); + } + } +} + #[derive(Default)] pub struct GroupBounds(HashMap; 1]>>); diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index bafedb7f2d..124c578b28 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -84,6 +84,15 @@ where self.base.layout(view_state, element_state, cx) } + fn prepaint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + } + fn paint( &mut self, bounds: Bounds, diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 5c5709d32e..0794ef431a 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -121,6 +121,15 @@ impl Element for Text { layout_id } + fn prepaint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + } + fn paint( &mut self, bounds: Bounds, diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 6687559d1c..16abbcf883 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -152,6 +152,16 @@ impl Element for UniformList { ) } + fn prepaint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + todo!() + } + fn paint( &mut self, bounds: crate::Bounds, @@ -229,7 +239,7 @@ impl Element for UniformList { let overflow = point(style.overflow.x, Overflow::Scroll); cx.with_z_index(0, |cx| { - self.interactivity.paint( + self.interactivity.handle_events( bounds, content_size, overflow, diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index aacaeac01f..f896bfc439 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -450,7 +450,7 @@ pub trait ElementInteractivity: 'static { } } - fn paint( + fn handle_events( &mut self, bounds: Bounds, content_size: Size, diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index d12d84f43b..9b2a06191f 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -147,6 +147,7 @@ pub struct AnyView { model: AnyModel, initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, + prepaint: fn(&AnyView, &mut AnyBox, &mut WindowContext), paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } @@ -156,6 +157,7 @@ impl AnyView { model: self.model.downgrade(), initialize: self.initialize, layout: self.layout, + prepaint: self.prepaint, paint: self.paint, } } @@ -167,6 +169,7 @@ impl AnyView { model, initialize: self.initialize, layout: self.layout, + prepaint: self.prepaint, paint: self.paint, }), } @@ -198,6 +201,7 @@ impl From> for AnyView { model: value.model.into_any(), initialize: any_view::initialize::, layout: any_view::layout::, + prepaint: any_view::prepaint::, paint: any_view::paint::, } } @@ -228,6 +232,16 @@ impl Element for AnyView { (self.layout)(self, rendered_element, cx) } + fn prepaint( + &mut self, + bounds: Bounds, + view_state: &mut ParentViewState, + rendered_element: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + (self.prepaint)(self, rendered_element, cx) + } + fn paint( &mut self, _bounds: Bounds, @@ -243,6 +257,7 @@ pub struct AnyWeakView { model: AnyWeakModel, initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, + prepaint: fn(&AnyView, &mut AnyBox, &mut WindowContext), paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } @@ -253,6 +268,7 @@ impl AnyWeakView { model, initialize: self.initialize, layout: self.layout, + prepaint: self.prepaint, paint: self.paint, }) } @@ -264,6 +280,7 @@ impl From> for AnyWeakView { model: view.model.into(), initialize: any_view::initialize::, layout: any_view::layout::, + prepaint: any_view::prepaint::, paint: any_view::paint::, } } @@ -309,6 +326,18 @@ mod any_view { }) } + pub(crate) fn prepaint( + view: &AnyView, + element: &mut Box, + cx: &mut WindowContext, + ) { + cx.with_element_id(view.model.entity_id, |_, cx| { + let view = view.clone().downcast::().unwrap(); + let element = element.downcast_mut::>().unwrap(); + view.update(cx, |view, cx| element.prepaint(view, cx)) + }) + } + pub(crate) fn paint( view: &AnyView, element: &mut Box, diff --git a/crates/ui2/src/components/modal.rs b/crates/ui2/src/components/modal.rs index 75528b5c34..805bbe95b2 100644 --- a/crates/ui2/src/components/modal.rs +++ b/crates/ui2/src/components/modal.rs @@ -1,4 +1,4 @@ -use gpui::AnyElement; +use gpui::{AnyElement, Pixels}; use smallvec::SmallVec; use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label}; From abdaa3105b77d38eae78ad782ca22c03f89da7dc Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 15:58:04 -0700 Subject: [PATCH 057/126] Update command matches faster --- crates/command_palette2/src/command_palette.rs | 11 +---------- crates/picker2/src/picker2.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index e403a50cf9..1386186ca4 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -130,16 +130,7 @@ impl CommandPaletteDelegate { ) -> Self { Self { command_palette, - matches: commands - .iter() - .enumerate() - .map(|(i, command)| StringMatch { - candidate_id: i, - string: command.name.clone(), - positions: Vec::new(), - score: 0.0, - }) - .collect(), + matches: vec![], commands, selected_ix: 0, previous_focus_handle, diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index e1979f1b13..836a612491 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -10,7 +10,7 @@ pub struct Picker { pub delegate: D, scroll_handle: UniformListScrollHandle, editor: View, - pending_update_matches: Option>>, + pending_update_matches: Option>, } pub trait PickerDelegate: Sized + 'static { @@ -42,12 +42,14 @@ impl Picker { editor }); cx.subscribe(&editor, Self::on_input_editor_event).detach(); - Self { + let mut this = Self { delegate, scroll_handle: UniformListScrollHandle::new(), pending_update_matches: None, editor, - } + }; + this.update_matches("".to_string(), cx); + this } pub fn focus(&self, cx: &mut WindowContext) { @@ -126,7 +128,7 @@ impl Picker { this.update(&mut cx, |this, cx| { this.matches_updated(cx); }) - .ok() + .ok(); })); } From 06f3c60be83614cf76dc6605505c288e22edb8f0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 18:56:59 -0700 Subject: [PATCH 058/126] Fix action dispatching... --- crates/editor2/src/editor.rs | 2 +- crates/gpui2/src/window.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 20b970d672..0995bbd534 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9383,7 +9383,7 @@ impl Render for Editor { color: cx.theme().colors().text, font_family: "Zed Sans".into(), // todo!() font_features: FontFeatures::default(), - font_size: rems(1.0).into(), + font_size: settings.ui_font_size.into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()), diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 425fb9510f..450b36ea81 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -424,6 +424,7 @@ impl<'a> WindowContext<'a> { .dispatch_tree .focusable_node_id(focus_handle.id) { + cx.propagate_event = true; cx.dispatch_action_on_node(node_id, action); } }) From 922bb3195b2f7f3fa83fc6e7fbefb8e95c24d9d9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 18:58:42 -0700 Subject: [PATCH 059/126] WIP --- crates/editor2/src/element.rs | 9 -- crates/gpui2/src/element.rs | 53 ------- crates/gpui2/src/elements/div.rs | 10 -- crates/gpui2/src/elements/img.rs | 9 -- crates/gpui2/src/elements/node.rs | 162 +++++++++++----------- crates/gpui2/src/elements/svg.rs | 9 -- crates/gpui2/src/elements/text.rs | 9 -- crates/gpui2/src/elements/uniform_list.rs | 10 -- crates/gpui2/src/view.rs | 29 ---- 9 files changed, 82 insertions(+), 218 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 38ec0f3d11..f8386ee271 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2640,15 +2640,6 @@ impl Element for EditorElement { cx.request_layout(&style, None) } - fn prepaint( - &mut self, - bounds: Bounds, - view_state: &mut Editor, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - } - fn paint( &mut self, bounds: Bounds, diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 1dc9c155eb..9ee9eaa7c3 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -26,14 +26,6 @@ pub trait Element { cx: &mut ViewContext, ) -> LayoutId; - fn prepaint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ); - fn paint( &mut self, bounds: Bounds, @@ -70,7 +62,6 @@ pub trait ParentElement { trait ElementObject { fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext); fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId; - fn prepaint(&mut self, view_state: &mut V, cx: &mut ViewContext); fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); fn measure( &mut self, @@ -208,36 +199,6 @@ where }; } - fn prepaint(&mut self, view_state: &mut V, cx: &mut ViewContext) { - self.phase = match mem::take(&mut self.phase) { - ElementRenderPhase::LayoutRequested { - layout_id, - mut frame_state, - } - | ElementRenderPhase::LayoutComputed { - layout_id, - mut frame_state, - .. - } => { - let bounds = cx.layout_bounds(layout_id); - if let Some(id) = self.element.id() { - cx.with_element_state(id, |element_state, cx| { - let mut element_state = element_state.unwrap(); - self.element - .prepaint(bounds, view_state, &mut element_state, cx); - ((), element_state) - }); - } else { - self.element - .prepaint(bounds, view_state, frame_state.as_mut().unwrap(), cx); - } - ElementRenderPhase::Painted - } - - _ => panic!("must call layout before paint"), - }; - } - fn measure( &mut self, available_space: Size, @@ -322,10 +283,6 @@ impl AnyElement { self.0.paint(view_state, cx) } - pub fn prepaint(&mut self, view_state: &mut V, cx: &mut ViewContext) { - self.0.prepaint(view_state, cx) - } - /// Initializes this element and performs layout within the given available space to determine its size. pub fn measure( &mut self, @@ -419,16 +376,6 @@ where rendered_element.layout(view_state, cx) } - fn prepaint( - &mut self, - _bounds: Bounds, - view_state: &mut V, - rendered_element: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - rendered_element.prepaint(view_state, cx) - } - fn paint( &mut self, _bounds: Bounds, diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 1211a6bc12..25c13d6980 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -265,16 +265,6 @@ where }) } - fn prepaint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - todo!() - } - fn paint( &mut self, bounds: Bounds, diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 137dde7dfe..1ff088c1af 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -94,15 +94,6 @@ where self.base.layout(view_state, element_state, cx) } - fn prepaint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - } - fn paint( &mut self, bounds: Bounds, diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index bcf3772aed..06d4ae2636 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -274,9 +274,7 @@ pub trait InteractiveComponent { } } -pub trait StatefulInteractiveComponent { - fn interactivity(&mut self) -> &mut StatefulInteractivity; - +pub trait StatefulInteractiveComponent: InteractiveComponent { fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -525,23 +523,14 @@ pub struct FocusEvent { } pub struct Node { - style: StyleRefinement, key_context: KeyContext, interactivity: Interactivity, children: Vec>, } -impl Node { - fn compute_style(&self) -> Style { - let mut style = Style::default(); - style.refine(&self.style); - style - } -} - impl Styled for Node { fn style(&mut self) -> &mut StyleRefinement { - &mut self.style + &mut self.interactivity.base_style } } @@ -582,7 +571,7 @@ impl Element for Node { element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> crate::LayoutId { - let style = self.compute_style(); + let style = self.interactivity().compute_style(None, cx); style.with_text_style(cx, |cx| { element_state.child_layout_ids = self .children @@ -593,20 +582,6 @@ impl Element for Node { }) } - fn prepaint( - &mut self, - bounds: Bounds, - view_state: &mut V, - _: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - for child in &mut self.children { - child.prepaint(view_state, cx); - } - self.interactivity - .refine_style(&mut self.style, bounds, view_state, cx); - } - fn paint( &mut self, bounds: Bounds, @@ -614,7 +589,7 @@ impl Element for Node { element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - let style = self.compute_style(); + let style = self.interactivity.compute_style(Some(bounds), cx); if style.visibility == Visibility::Hidden { return; } @@ -671,12 +646,37 @@ impl Element for Node { } } +pub struct ComputedStyle { + base: StyleRefinement, + focus: StyleRefinement, + hover: StyleRefinement, + active: StyleRefinement, +} + +pub struct StyleCascade { + pub base: StyleRefinement, + pub focus: StyleRefinement, + pub hover: StyleRefinement, + pub dragged_over: StyleRefinement, + pub active: StyleRefinement, +} + pub struct Interactivity { + pub active: bool, + pub group_active: bool, + pub hovered: bool, + pub group_hovered: bool, + pub focused: bool, + pub scroll_offset: Point, + pub base_style: StyleRefinement, + pub focus_style: StyleRefinement, pub hover_style: StyleRefinement, pub group_hover_style: Option, + pub active_style: StyleRefinement, + pub group_active_style: Option, pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, - group: Option, + pub group: Option, pub dispatch_context: KeyContext, pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, @@ -685,49 +685,68 @@ pub struct Interactivity { pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, - drop_listeners: SmallVec<[(TypeId, Box>); 2]>, - scroll_offset: Point, + pub drop_listeners: SmallVec<[(TypeId, Box>); 2]>, + pub click_listeners: SmallVec<[ClickListener; 2]>, + pub drag_listener: Option>, + pub hover_listener: Option>, + pub tooltip_builder: Option>, } impl Interactivity { - fn refine_style( - &self, - style: &mut StyleRefinement, - bounds: Bounds, - cx: &mut ViewContext, - ) { - let mouse_position = cx.mouse_position(); - if let Some(group_hover) = self.group_hover_style.as_ref() { - if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { - if group_bounds.contains_point(&mouse_position) { - style.refine(&group_hover.style); - } - } - } - if bounds.contains_point(&mouse_position) { - style.refine(&self.hover_style); + fn compute_style(&self, bounds: Option>, cx: &mut ViewContext) -> Style { + let mut style = Style::default(); + style.refine(&self.base_style); + if self.focused { + style.refine(&self.focus_style); } - if let Some(drag) = cx.active_drag.take() { - for (state_type, group_drag_style) in &self.group_drag_over_styles { - if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { - if *state_type == drag.view.entity_type() - && group_bounds.contains_point(&mouse_position) - { - style.refine(&group_drag_style.style); + if let Some(bounds) = bounds { + let mouse_position = cx.mouse_position(); + if let Some(group_hover) = self.group_hover_style.as_ref() { + if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { + if group_bounds.contains_point(&mouse_position) { + style.refine(&group_hover.style); } } } - - for (state_type, drag_over_style) in &self.drag_over_styles { - if *state_type == drag.view.entity_type() && bounds.contains_point(&mouse_position) - { - style.refine(drag_over_style); - } + if bounds.contains_point(&mouse_position) { + style.refine(&self.hover_style); } - cx.active_drag = Some(drag); + if let Some(drag) = cx.active_drag.take() { + for (state_type, group_drag_style) in &self.group_drag_over_styles { + if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { + if *state_type == drag.view.entity_type() + && group_bounds.contains_point(&mouse_position) + { + style.refine(&group_drag_style.style); + } + } + } + + for (state_type, drag_over_style) in &self.drag_over_styles { + if *state_type == drag.view.entity_type() + && bounds.contains_point(&mouse_position) + { + style.refine(drag_over_style); + } + } + + cx.active_drag = Some(drag); + } } + + if self.group_active { + if let Some(group) = self.group_active_style.as_ref() { + style.refine(&group.style) + } + } + + if self.active { + style.refine(&self.active_style) + } + + style } fn paint(&mut self, bounds: Bounds, cx: &mut ViewContext) { @@ -863,32 +882,15 @@ impl> InteractiveComponent for Focusab impl> StatefulInteractiveComponent for Focusable { - fn interactivity(&mut self) -> &mut StatefulInteractivity { - self.element.interactivity() - } } pub struct Stateful { id: SharedString, - interactivity: StatefulInteractivity, view_type: PhantomData, element: E, } -pub struct StatefulInteractivity { - click_listeners: SmallVec<[ClickListener; 2]>, - active_style: StyleRefinement, - group_active_style: Option, - drag_listener: Option>, - hover_listener: Option>, - tooltip_builder: Option>, -} - -impl StatefulInteractiveComponent for Stateful { - fn interactivity(&mut self) -> &mut StatefulInteractivity { - &mut self.interactivity - } -} +impl> StatefulInteractiveComponent for Stateful {} impl> InteractiveComponent for Stateful { fn interactivity(&mut self) -> &mut Interactivity { diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index 124c578b28..bafedb7f2d 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -84,15 +84,6 @@ where self.base.layout(view_state, element_state, cx) } - fn prepaint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - } - fn paint( &mut self, bounds: Bounds, diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 0794ef431a..5c5709d32e 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -121,15 +121,6 @@ impl Element for Text { layout_id } - fn prepaint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - } - fn paint( &mut self, bounds: Bounds, diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 16abbcf883..521df8699a 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -152,16 +152,6 @@ impl Element for UniformList { ) } - fn prepaint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - todo!() - } - fn paint( &mut self, bounds: crate::Bounds, diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 9b2a06191f..d12d84f43b 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -147,7 +147,6 @@ pub struct AnyView { model: AnyModel, initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, - prepaint: fn(&AnyView, &mut AnyBox, &mut WindowContext), paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } @@ -157,7 +156,6 @@ impl AnyView { model: self.model.downgrade(), initialize: self.initialize, layout: self.layout, - prepaint: self.prepaint, paint: self.paint, } } @@ -169,7 +167,6 @@ impl AnyView { model, initialize: self.initialize, layout: self.layout, - prepaint: self.prepaint, paint: self.paint, }), } @@ -201,7 +198,6 @@ impl From> for AnyView { model: value.model.into_any(), initialize: any_view::initialize::, layout: any_view::layout::, - prepaint: any_view::prepaint::, paint: any_view::paint::, } } @@ -232,16 +228,6 @@ impl Element for AnyView { (self.layout)(self, rendered_element, cx) } - fn prepaint( - &mut self, - bounds: Bounds, - view_state: &mut ParentViewState, - rendered_element: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - (self.prepaint)(self, rendered_element, cx) - } - fn paint( &mut self, _bounds: Bounds, @@ -257,7 +243,6 @@ pub struct AnyWeakView { model: AnyWeakModel, initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, - prepaint: fn(&AnyView, &mut AnyBox, &mut WindowContext), paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } @@ -268,7 +253,6 @@ impl AnyWeakView { model, initialize: self.initialize, layout: self.layout, - prepaint: self.prepaint, paint: self.paint, }) } @@ -280,7 +264,6 @@ impl From> for AnyWeakView { model: view.model.into(), initialize: any_view::initialize::, layout: any_view::layout::, - prepaint: any_view::prepaint::, paint: any_view::paint::, } } @@ -326,18 +309,6 @@ mod any_view { }) } - pub(crate) fn prepaint( - view: &AnyView, - element: &mut Box, - cx: &mut WindowContext, - ) { - cx.with_element_id(view.model.entity_id, |_, cx| { - let view = view.clone().downcast::().unwrap(); - let element = element.downcast_mut::>().unwrap(); - view.update(cx, |view, cx| element.prepaint(view, cx)) - }) - } - pub(crate) fn paint( view: &AnyView, element: &mut Box, From 872b5186e20db43727f9cf055a30d7c9fd0841e5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 19:23:07 -0700 Subject: [PATCH 060/126] Checkpoint --- crates/gpui2/src/elements/node.rs | 132 ++++++++++++++++++++++-------- 1 file changed, 98 insertions(+), 34 deletions(-) diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index 06d4ae2636..7133997c48 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -386,7 +386,7 @@ pub trait FocusableComponent { where Self: Sized, { - // self.focusability(). (f(StyleRefinement::default())); + self.focusability().in_focus_style = f(StyleRefinement::default()); self } @@ -523,7 +523,6 @@ pub struct FocusEvent { } pub struct Node { - key_context: KeyContext, interactivity: Interactivity, children: Vec>, } @@ -621,24 +620,27 @@ impl Element for Node { (child_max - child_min).into() }; - cx.with_z_index(z_index, |cx| { - cx.with_z_index(0, |cx| { - style.paint(bounds, cx); - self.interactivity.paint(bounds, cx); - }); - cx.with_z_index(1, |cx| { - style.with_text_style(cx, |cx| { - style.apply_overflow(bounds, cx, |cx| { - let scroll_offset = self.interactivity.scroll_offset; - cx.with_element_offset2(scroll_offset, |cx| { - for child in &mut self.children { - child.paint(view_state, cx); - } - }); + let mut interactivity = mem::take(&mut self.interactivity); + interactivity.paint(bounds, cx, |cx| { + cx.with_z_index(z_index, |cx| { + cx.with_z_index(0, |cx| { + style.paint(bounds, cx); + }); + cx.with_z_index(1, |cx| { + style.with_text_style(cx, |cx| { + style.apply_overflow(bounds, cx, |cx| { + let scroll_offset = self.interactivity.scroll_offset; + cx.with_element_offset2(scroll_offset, |cx| { + for child in &mut self.children { + child.paint(view_state, cx); + } + }); + }) }) - }) + }); }); }); + self.interactivity = interactivity; if let Some(group) = self.interactivity.group.as_ref() { GroupBounds::pop(group, cx); @@ -646,19 +648,15 @@ impl Element for Node { } } -pub struct ComputedStyle { - base: StyleRefinement, - focus: StyleRefinement, - hover: StyleRefinement, - active: StyleRefinement, -} - -pub struct StyleCascade { - pub base: StyleRefinement, - pub focus: StyleRefinement, - pub hover: StyleRefinement, - pub dragged_over: StyleRefinement, - pub active: StyleRefinement, +pub enum FocusState { + /// The current element is not focused, and does not contain or descend from the focused element. + None, + /// The current element is focused. + Focus, + /// The current element contains the focused element + FocusIn, + /// The current element descends from the focused element + InFocus, } pub struct Interactivity { @@ -666,10 +664,14 @@ pub struct Interactivity { pub group_active: bool, pub hovered: bool, pub group_hovered: bool, - pub focused: bool, + pub focus: FocusState, + pub key_context: KeyContext, + pub focus_handle: Option, pub scroll_offset: Point, pub base_style: StyleRefinement, pub focus_style: StyleRefinement, + pub focus_in_style: StyleRefinement, + pub in_focus_style: StyleRefinement, pub hover_style: StyleRefinement, pub group_hover_style: Option, pub active_style: StyleRefinement, @@ -696,8 +698,20 @@ impl Interactivity { fn compute_style(&self, bounds: Option>, cx: &mut ViewContext) -> Style { let mut style = Style::default(); style.refine(&self.base_style); - if self.focused { - style.refine(&self.focus_style); + + match self.focus { + FocusState::None => {} + FocusState::Focus => { + style.refine(&self.focus_style); + style.refine(&self.focus_in_style); + style.refine(&self.in_focus_style); + } + FocusState::FocusIn => { + style.refine(&self.focus_in_style); + } + FocusState::InFocus => { + style.refine(&self.in_focus_style); + } } if let Some(bounds) = bounds { @@ -749,7 +763,12 @@ impl Interactivity { style } - fn paint(&mut self, bounds: Bounds, cx: &mut ViewContext) { + fn paint( + &mut self, + bounds: Bounds, + cx: &mut ViewContext, + f: impl FnOnce(&mut ViewContext), + ) { for listener in self.mouse_down_listeners.drain(..) { cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { listener(state, event, &bounds, phase, cx); @@ -825,6 +844,51 @@ impl Interactivity { } }); } + + cx.with_key_dispatch( + self.key_context.clone(), + self.focus_handle.clone(), + |_, cx| f(cx), + ); + } +} + +impl Default for Interactivity { + fn default() -> Self { + Self { + active: false, + group_active: false, + hovered: false, + group_hovered: false, + focus: FocusState::None, + key_context: KeyContext::default(), + focus_handle: None, + scroll_offset: Point::default(), + base_style: StyleRefinement::default(), + focus_style: StyleRefinement::default(), + focus_in_style: StyleRefinement::default(), + in_focus_style: StyleRefinement::default(), + hover_style: StyleRefinement::default(), + group_hover_style: None, + active_style: StyleRefinement::default(), + group_active_style: None, + drag_over_styles: SmallVec::new(), + group_drag_over_styles: SmallVec::new(), + group: None, + dispatch_context: KeyContext::default(), + mouse_down_listeners: SmallVec::new(), + mouse_up_listeners: SmallVec::new(), + mouse_move_listeners: SmallVec::new(), + scroll_wheel_listeners: SmallVec::new(), + key_down_listeners: SmallVec::new(), + key_up_listeners: SmallVec::new(), + action_listeners: SmallVec::new(), + drop_listeners: SmallVec::new(), + click_listeners: SmallVec::new(), + drag_listener: None, + hover_listener: None, + tooltip_builder: None, + } } } From c5878cbd5f857f63bc5ad3a1ff2b42ea6e7b178f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 19:31:40 -0700 Subject: [PATCH 061/126] Add Text::styled() and use it in command palette Prevents jumping while typing --- crates/gpui2/src/elements/text.rs | 29 +++++++++++++++++++++++++++-- crates/gpui2/src/text_system.rs | 1 + crates/ui2/src/components/label.rs | 27 +++++++++++++-------------- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index eee0584460..5d91b23bf3 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,6 +1,6 @@ use crate::{ AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString, - Size, ViewContext, + Size, TextRun, ViewContext, }; use parking_lot::Mutex; use smallvec::SmallVec; @@ -11,6 +11,7 @@ impl Component for SharedString { fn render(self) -> AnyElement { Text { text: self, + runs: None, state_type: PhantomData, } .render() @@ -21,6 +22,7 @@ impl Component for &'static str { fn render(self) -> AnyElement { Text { text: self.into(), + runs: None, state_type: PhantomData, } .render() @@ -33,6 +35,7 @@ impl Component for String { fn render(self) -> AnyElement { Text { text: self.into(), + runs: None, state_type: PhantomData, } .render() @@ -41,9 +44,25 @@ impl Component for String { pub struct Text { text: SharedString, + runs: Option>, state_type: PhantomData, } +impl Text { + /// styled renders text that has different runs of different styles. + /// callers are responsible for setting the correct style for each run. + //// + /// For uniform text you can usually just pass a string as a child, and + /// cx.text_style() will be used automatically. + pub fn styled(text: SharedString, runs: Vec) -> Self { + Text { + text, + runs: Some(runs), + state_type: Default::default(), + } + } +} + impl Component for Text { fn render(self) -> AnyElement { AnyElement::new(self) @@ -82,6 +101,12 @@ impl Element for Text { let rem_size = cx.rem_size(); + let runs = if let Some(runs) = self.runs.take() { + runs + } else { + vec![text_style.to_run(text.len())] + }; + let layout_id = cx.request_measured_layout(Default::default(), rem_size, { let element_state = element_state.clone(); move |known_dimensions, _| { @@ -89,7 +114,7 @@ impl Element for Text { .layout_text( &text, font_size, - &[text_style.to_run(text.len())], + &runs[..], known_dimensions.width, // Wrap if we know the width. ) .log_err() diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index dd0689396e..e8d6acc5a3 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -368,6 +368,7 @@ impl Display for FontStyle { #[derive(Clone, Debug, PartialEq, Eq)] pub struct TextRun { + // number of utf8 bytes pub len: usize, pub font: Font, pub color: Hsla, diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 827ba87918..6b915af1b9 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -1,5 +1,4 @@ -use gpui::{relative, Hsla, WindowContext}; -use smallvec::SmallVec; +use gpui::{relative, Hsla, Text, TextRun, WindowContext}; use crate::prelude::*; use crate::styled_ext::StyledExt; @@ -105,6 +104,8 @@ pub struct HighlightedLabel { } impl HighlightedLabel { + /// shows a label with the given characters highlighted. + /// characters are identified by utf8 byte position. pub fn new(label: impl Into, highlight_indices: Vec) -> Self { Self { label: label.into(), @@ -126,10 +127,11 @@ impl HighlightedLabel { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let highlight_color = cx.theme().colors().text_accent; + let mut text_style = cx.text_style().clone(); let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); - let mut runs: SmallVec<[Run; 8]> = SmallVec::new(); + let mut runs: Vec = Vec::new(); for (char_ix, char) in self.label.char_indices() { let mut color = self.color.hsla(cx); @@ -137,16 +139,14 @@ impl HighlightedLabel { if let Some(highlight_ix) = highlight_indices.peek() { if char_ix == *highlight_ix { color = highlight_color; - highlight_indices.next(); } } let last_run = runs.last_mut(); - let start_new_run = if let Some(last_run) = last_run { if color == last_run.color { - last_run.text.push(char); + last_run.len += char.len_utf8(); false } else { true @@ -156,10 +156,8 @@ impl HighlightedLabel { }; if start_new_run { - runs.push(Run { - text: char.to_string(), - color, - }); + text_style.color = color; + runs.push(text_style.to_run(char.len_utf8())) } } @@ -176,10 +174,7 @@ impl HighlightedLabel { .bg(LabelColor::Hidden.hsla(cx)), ) }) - .children( - runs.into_iter() - .map(|run| div().text_color(run.color).child(run.text)), - ) + .child(Text::styled(self.label, runs)) } } @@ -213,6 +208,10 @@ mod stories { "Hello, world!", vec![0, 1, 2, 7, 8, 12], )) + .child(HighlightedLabel::new( + "Héllo, world!", + vec![0, 1, 3, 8, 9, 13], + )) } } } From 044d9679abc53c1dc0865a322ce0029fd7361cfe Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 21:40:02 -0700 Subject: [PATCH 062/126] Checkpoint --- crates/editor2/src/element.rs | 4 +- crates/gpui2/src/element.rs | 2 +- crates/gpui2/src/elements/div.rs | 6 +- crates/gpui2/src/elements/node.rs | 582 ++++++++++++++++++++++++------ crates/gpui2/src/gpui2.rs | 16 +- crates/gpui2/src/style.rs | 44 ++- crates/gpui2/src/window.rs | 47 +-- 7 files changed, 548 insertions(+), 153 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index f8386ee271..630b062d1f 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -616,7 +616,7 @@ impl EditorElement { let line_end_overshoot = 0.15 * layout.position_map.line_height; let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces; - cx.with_content_mask(ContentMask { bounds }, |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { // todo!("cursor region") // cx.scene().push_cursor_region(CursorRegion { // bounds, @@ -2659,7 +2659,7 @@ impl Element for EditorElement { // We call with_z_index to establish a new stacking context. cx.with_z_index(0, |cx| { - cx.with_content_mask(ContentMask { bounds }, |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { self.paint_mouse_listeners( bounds, gutter_bounds, diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 9ee9eaa7c3..80ab0abc90 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -255,7 +255,7 @@ where // Ignore the element offset when drawing this element, as the origin is already specified // in absolute terms. origin -= cx.element_offset(); - cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx)) + cx.with_element_offset(origin, |cx| self.paint(view_state, cx)) } } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 25c13d6980..2b411f7609 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -252,7 +252,7 @@ where cx: &mut ViewContext, ) -> LayoutId { let style = self.compute_style(Bounds::default(), element_state, cx); - style.with_text_style(cx, |cx| { + style.apply_text_style(cx, |cx| { self.with_element_id(cx, |this, _global_id, cx| { let layout_ids = this .children @@ -318,10 +318,10 @@ where ); }); cx.with_z_index(1, |cx| { - style.with_text_style(cx, |cx| { + style.apply_text_style(cx, |cx| { style.apply_overflow(bounds, cx, |cx| { let scroll_offset = element_state.interactive.scroll_offset(); - cx.with_element_offset(scroll_offset, |cx| { + cx.with_element_offset(scroll_offset.unwrap_or_default(), |cx| { for child in &mut this.children { child.paint(view_state, cx); } diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index 7133997c48..57f825994f 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -1,10 +1,12 @@ use crate::{ - point, Action, AnyDrag, AnyElement, AnyView, AppContext, BorrowWindow, Bounds, ClickEvent, - DispatchPhase, Element, FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, - ScrollWheelEvent, SharedString, Style, StyleRefinement, Styled, View, ViewContext, Visibility, + point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, + BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, FocusHandle, KeyContext, + KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, + Task, View, ViewContext, Visibility, }; use collections::HashMap; +use parking_lot::Mutex; use refineable::Refineable; use smallvec::SmallVec; use std::{ @@ -12,14 +14,20 @@ use std::{ marker::PhantomData, mem, sync::Arc, + time::Duration, }; +use taffy::style::Overflow; + +const DRAG_THRESHOLD: f64 = 2.; +const TOOLTIP_DELAY: Duration = Duration::from_millis(500); +const TOOLTIP_OFFSET: Point = Point::new(px(10.0), px(8.0)); pub struct GroupStyle { pub group: SharedString, pub style: StyleRefinement, } -pub trait InteractiveComponent { +pub trait InteractiveComponent: Sized + Element { fn interactivity(&mut self) -> &mut Interactivity; fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self @@ -274,7 +282,7 @@ pub trait InteractiveComponent { } } -pub trait StatefulInteractiveComponent: InteractiveComponent { +pub trait StatefulInteractiveComponent>: InteractiveComponent { fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -541,6 +549,13 @@ impl InteractiveComponent for Node { pub struct NodeState { child_layout_ids: SmallVec<[LayoutId; 4]>, + interactive_state: InteractiveElementState, +} + +impl AsMut for InteractiveElementState { + fn as_mut(&mut self) -> &mut InteractiveElementState { + self + } } impl Element for Node { @@ -553,7 +568,7 @@ impl Element for Node { fn initialize( &mut self, view_state: &mut V, - _: Option, + previous_element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { for child in &mut self.children { @@ -561,6 +576,9 @@ impl Element for Node { } NodeState { child_layout_ids: SmallVec::new(), + interactive_state: previous_element_state + .map(|s| s.interactive_state) + .unwrap_or_default(), } } @@ -570,15 +588,20 @@ impl Element for Node { element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> crate::LayoutId { - let style = self.interactivity().compute_style(None, cx); - style.with_text_style(cx, |cx| { - element_state.child_layout_ids = self - .children - .iter_mut() - .map(|child| child.layout(view_state, cx)) - .collect::>(); - cx.request_layout(&style, element_state.child_layout_ids.iter().copied()) - }) + let mut interactivity = mem::take(&mut self.interactivity); + let layout_id = + interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| { + cx.with_text_style(style.text_style().cloned(), |cx| { + element_state.child_layout_ids = self + .children + .iter_mut() + .map(|child| child.layout(view_state, cx)) + .collect::>(); + cx.request_layout(&style, element_state.child_layout_ids.iter().copied()) + }) + }); + self.interactivity = interactivity; + layout_id } fn paint( @@ -588,27 +611,10 @@ impl Element for Node { element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - let style = self.interactivity.compute_style(Some(bounds), cx); - if style.visibility == Visibility::Hidden { - return; - } - - if let Some(mouse_cursor) = style.mouse_cursor { - let hovered = bounds.contains_point(&cx.mouse_position()); - if hovered { - cx.set_cursor_style(mouse_cursor); - } - } - - if let Some(group) = self.interactivity.group.clone() { - GroupBounds::push(group, bounds, cx); - } - - let z_index = style.z_index.unwrap_or(0); + let mut interactivity = mem::take(&mut self.interactivity); let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); - let content_size = if element_state.child_layout_ids.is_empty() { bounds.size } else { @@ -620,35 +626,41 @@ impl Element for Node { (child_max - child_min).into() }; - let mut interactivity = mem::take(&mut self.interactivity); - interactivity.paint(bounds, cx, |cx| { - cx.with_z_index(z_index, |cx| { - cx.with_z_index(0, |cx| { - style.paint(bounds, cx); - }); - cx.with_z_index(1, |cx| { - style.with_text_style(cx, |cx| { - style.apply_overflow(bounds, cx, |cx| { - let scroll_offset = self.interactivity.scroll_offset; - cx.with_element_offset2(scroll_offset, |cx| { - for child in &mut self.children { - child.paint(view_state, cx); - } - }); + interactivity.paint( + bounds, + content_size, + &mut element_state.interactive_state, + cx, + |style, scroll_offset, cx| { + if style.visibility == Visibility::Hidden { + return; + } + + let z_index = style.z_index.unwrap_or(0); + + cx.with_z_index(z_index, |cx| { + cx.with_z_index(0, |cx| { + style.paint(bounds, cx); + }); + cx.with_z_index(1, |cx| { + cx.with_text_style(style.text_style().cloned(), |cx| { + cx.with_content_mask(style.overflow_mask(bounds), |cx| { + cx.with_element_offset(scroll_offset, |cx| { + for child in &mut self.children { + child.paint(view_state, cx); + } + }) + }) }) }) - }); - }); - }); + }) + }, + ); self.interactivity = interactivity; - - if let Some(group) = self.interactivity.group.as_ref() { - GroupBounds::pop(group, cx); - } } } -pub enum FocusState { +pub enum FocusStatus { /// The current element is not focused, and does not contain or descend from the focused element. None, /// The current element is focused. @@ -660,56 +672,87 @@ pub enum FocusState { } pub struct Interactivity { - pub active: bool, - pub group_active: bool, - pub hovered: bool, - pub group_hovered: bool, - pub focus: FocusState, - pub key_context: KeyContext, - pub focus_handle: Option, - pub scroll_offset: Point, - pub base_style: StyleRefinement, - pub focus_style: StyleRefinement, - pub focus_in_style: StyleRefinement, - pub in_focus_style: StyleRefinement, - pub hover_style: StyleRefinement, - pub group_hover_style: Option, - pub active_style: StyleRefinement, - pub group_active_style: Option, - pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, - pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, - pub group: Option, - pub dispatch_context: KeyContext, - pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, - pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, - pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, - pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, - pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, - pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, - pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, - pub drop_listeners: SmallVec<[(TypeId, Box>); 2]>, - pub click_listeners: SmallVec<[ClickListener; 2]>, - pub drag_listener: Option>, - pub hover_listener: Option>, - pub tooltip_builder: Option>, + active: Option, + group_active: bool, + hovered: bool, + group_hovered: bool, + focus_status: FocusStatus, + key_context: KeyContext, + focus_handle: Option, + scroll_offset: Point, + base_style: StyleRefinement, + focus_style: StyleRefinement, + focus_in_style: StyleRefinement, + in_focus_style: StyleRefinement, + hover_style: StyleRefinement, + group_hover_style: Option, + active_style: StyleRefinement, + group_active_style: Option, + drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, + group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, + group: Option, + dispatch_context: KeyContext, + mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, + mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, + mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, + scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, + key_down_listeners: SmallVec<[KeyDownListener; 2]>, + key_up_listeners: SmallVec<[KeyUpListener; 2]>, + action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, + drop_listeners: SmallVec<[(TypeId, Box>); 2]>, + click_listeners: SmallVec<[ClickListener; 2]>, + drag_listener: Option>, + hover_listener: Option>, + tooltip_builder: Option>, } -impl Interactivity { +#[derive(Default)] +pub struct InteractiveElementState { + clicked_state: Arc>, + hover_state: Arc>, + pending_mouse_down: Arc>>, + scroll_offset: Option>>>, + active_tooltip: Arc>>, +} + +struct ActiveTooltip { + #[allow(unused)] // used to drop the task + waiting: Option>, + tooltip: Option, +} + +/// Whether or not the element or a group that contains it is clicked by the mouse. +#[derive(Copy, Clone, Default, Eq, PartialEq)] +struct ElementClickedState { + pub group: bool, + pub element: bool, +} + +impl ElementClickedState { + fn is_clicked(&self) -> bool { + self.group || self.element + } +} + +impl Interactivity +where + V: 'static, +{ fn compute_style(&self, bounds: Option>, cx: &mut ViewContext) -> Style { let mut style = Style::default(); style.refine(&self.base_style); - match self.focus { - FocusState::None => {} - FocusState::Focus => { + match self.focus_status { + FocusStatus::None => {} + FocusStatus::Focus => { style.refine(&self.focus_style); style.refine(&self.focus_in_style); style.refine(&self.in_focus_style); } - FocusState::FocusIn => { + FocusStatus::FocusIn => { style.refine(&self.focus_in_style); } - FocusState::InFocus => { + FocusStatus::InFocus => { style.refine(&self.in_focus_style); } } @@ -756,19 +799,66 @@ impl Interactivity { } } - if self.active { + if self.active.is_some() { style.refine(&self.active_style) } style } + fn layout( + &mut self, + element_state: &mut InteractiveElementState, + cx: &mut ViewContext, + f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, + ) -> LayoutId { + let mut style = Style::default(); + style.refine(&self.base_style); + + if let Some(focus_handle) = self.focus_handle.as_ref() { + if focus_handle.contains_focused(cx) { + style.refine(&self.focus_in_style); + } + + if focus_handle.within_focused(cx) { + style.refine(&self.in_focus_style); + } + + if focus_handle.is_focused(cx) { + style.refine(&self.focus_style); + } + } + + let clicked_state = element_state.clicked_state.lock(); + if clicked_state.group { + if let Some(group_style) = self.group_active_style.as_ref() { + style.refine(&group_style.style); + } + } + if clicked_state.element { + style.refine(&self.active_style); + } + + f(style, cx) + } + fn paint( &mut self, bounds: Bounds, + content_size: Size, + element_state: &mut InteractiveElementState, cx: &mut ViewContext, - f: impl FnOnce(&mut ViewContext), + f: impl FnOnce(Style, Point, &mut ViewContext), ) { + let style = self.compute_style(Some(bounds), cx); + + if let Some(mouse_cursor) = style.mouse_cursor { + let hovered = bounds.contains_point(&cx.mouse_position()); + if hovered { + cx.set_cursor_style(mouse_cursor); + } + } + for listener in self.mouse_down_listeners.drain(..) { cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { listener(state, event, &bounds, phase, cx); @@ -845,22 +935,212 @@ impl Interactivity { }); } + let mut element_state: &mut InteractiveElementState = element_state.as_mut(); + + let click_listeners = mem::take(&mut self.click_listeners); + let drag_listener = mem::take(&mut self.drag_listener); + + if !click_listeners.is_empty() || drag_listener.is_some() { + let pending_mouse_down = element_state.pending_mouse_down.clone(); + let mouse_down = pending_mouse_down.lock().clone(); + if let Some(mouse_down) = mouse_down { + if let Some(drag_listener) = drag_listener { + let active_state = element_state.clicked_state.clone(); + + cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { + if cx.active_drag.is_some() { + if phase == DispatchPhase::Capture { + cx.notify(); + } + } else if phase == DispatchPhase::Bubble + && bounds.contains_point(&event.position) + && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD + { + *active_state.lock() = ElementClickedState::default(); + let cursor_offset = event.position - bounds.origin; + let drag = drag_listener(view_state, cursor_offset, cx); + cx.active_drag = Some(drag); + cx.notify(); + cx.stop_propagation(); + } + }); + } + + cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + let mouse_click = ClickEvent { + down: mouse_down.clone(), + up: event.clone(), + }; + for listener in &click_listeners { + listener(view_state, &mouse_click, cx); + } + } + *pending_mouse_down.lock() = None; + }); + } else { + cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + *pending_mouse_down.lock() = Some(event.clone()); + } + }); + } + } + + if let Some(hover_listener) = self.hover_listener.take() { + let was_hovered = element_state.hover_state.clone(); + let has_mouse_down = element_state.pending_mouse_down.clone(); + + cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } + let is_hovered = + bounds.contains_point(&event.position) && has_mouse_down.lock().is_none(); + let mut was_hovered = was_hovered.lock(); + + if is_hovered != was_hovered.clone() { + *was_hovered = is_hovered; + drop(was_hovered); + + hover_listener(view_state, is_hovered, cx); + } + }); + } + + if let Some(tooltip_builder) = self.tooltip_builder.take() { + let active_tooltip = element_state.active_tooltip.clone(); + let pending_mouse_down = element_state.pending_mouse_down.clone(); + + cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } + + let is_hovered = + bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none(); + if !is_hovered { + active_tooltip.lock().take(); + return; + } + + if active_tooltip.lock().is_none() { + let task = cx.spawn({ + let active_tooltip = active_tooltip.clone(); + let tooltip_builder = tooltip_builder.clone(); + + move |view, mut cx| async move { + cx.background_executor().timer(TOOLTIP_DELAY).await; + view.update(&mut cx, move |view_state, cx| { + active_tooltip.lock().replace(ActiveTooltip { + waiting: None, + tooltip: Some(AnyTooltip { + view: tooltip_builder(view_state, cx), + cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET, + }), + }); + cx.notify(); + }) + .ok(); + } + }); + active_tooltip.lock().replace(ActiveTooltip { + waiting: Some(task), + tooltip: None, + }); + } + }); + + if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { + if active_tooltip.tooltip.is_some() { + cx.active_tooltip = active_tooltip.tooltip.clone() + } + } + } + + let active_state = element_state.clicked_state.clone(); + if !active_state.lock().is_clicked() { + cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Capture { + *active_state.lock() = ElementClickedState::default(); + cx.notify(); + } + }); + } else { + let active_group_bounds = self + .group_active_style + .as_ref() + .and_then(|group_active| GroupBounds::get(&group_active.group, cx)); + cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble { + let group = active_group_bounds + .map_or(false, |bounds| bounds.contains_point(&down.position)); + let element = bounds.contains_point(&down.position); + if group || element { + *active_state.lock() = ElementClickedState { group, element }; + cx.notify(); + } + } + }); + } + + let overflow = style.overflow; + if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll { + let scroll_offset = element_state + .scroll_offset + .get_or_insert_with(Arc::default) + .clone(); + let line_height = cx.line_height(); + let scroll_max = (content_size - bounds.size).max(&Size::default()); + + cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + let mut scroll_offset = scroll_offset.lock(); + let old_scroll_offset = *scroll_offset; + let delta = event.delta.pixel_delta(line_height); + + if overflow.x == Overflow::Scroll { + scroll_offset.x = + (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.)); + } + + if overflow.y == Overflow::Scroll { + scroll_offset.y = + (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.)); + } + + if *scroll_offset != old_scroll_offset { + cx.notify(); + cx.stop_propagation(); + } + } + }); + } + + if let Some(group) = self.group.clone() { + GroupBounds::push(group, bounds, cx); + } + cx.with_key_dispatch( self.key_context.clone(), self.focus_handle.clone(), - |_, cx| f(cx), + |_, cx| f(style, self.scroll_offset, cx), ); + + if let Some(group) = self.group.as_ref() { + GroupBounds::pop(group, cx); + } } } impl Default for Interactivity { fn default() -> Self { Self { - active: false, + active: None, group_active: false, hovered: false, group_hovered: false, - focus: FocusState::None, + focus_status: FocusStatus::None, key_context: KeyContext::default(), focus_handle: None, scroll_offset: Point::default(), @@ -937,26 +1217,80 @@ impl FocusableComponent for Focusable { } } -impl> InteractiveComponent for Focusable { +impl InteractiveComponent for Focusable +where + V: 'static, + E: InteractiveComponent, +{ fn interactivity(&mut self) -> &mut Interactivity { self.element.interactivity() } } -impl> StatefulInteractiveComponent +impl> StatefulInteractiveComponent for Focusable { } +impl Element for Focusable +where + V: 'static, + E: Element, +{ + type ElementState = E::ElementState; + + fn id(&self) -> Option { + todo!() + } + + fn initialize( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut ViewContext, + ) -> Self::ElementState { + todo!() + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) -> LayoutId { + todo!() + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + todo!() + } +} + pub struct Stateful { id: SharedString, view_type: PhantomData, element: E, } -impl> StatefulInteractiveComponent for Stateful {} +impl StatefulInteractiveComponent for Stateful +where + V: 'static, + E: Element, + Self: InteractiveComponent, +{ +} -impl> InteractiveComponent for Stateful { +impl InteractiveComponent for Stateful +where + V: 'static, + E: InteractiveComponent, +{ fn interactivity(&mut self) -> &mut Interactivity { self.element.interactivity() } @@ -967,3 +1301,43 @@ impl> FocusableComponent for Stateful { self.element.focusability() } } + +impl Element for Stateful +where + V: 'static, + E: Element, +{ + type ElementState = InteractiveElementState; + + fn id(&self) -> Option { + todo!() + } + + fn initialize( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut ViewContext, + ) -> Self::ElementState { + todo!() + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) -> LayoutId { + todo!() + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + todo!() + } +} diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 87de7998a8..323ca5851b 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -148,7 +148,7 @@ pub enum GlobalKey { } pub trait BorrowAppContext { - fn with_text_style(&mut self, style: TextStyleRefinement, f: F) -> R + fn with_text_style(&mut self, style: Option, f: F) -> R where F: FnOnce(&mut Self) -> R; @@ -159,14 +159,18 @@ impl BorrowAppContext for C where C: BorrowMut, { - fn with_text_style(&mut self, style: TextStyleRefinement, f: F) -> R + fn with_text_style(&mut self, style: Option, f: F) -> R where F: FnOnce(&mut Self) -> R, { - self.borrow_mut().push_text_style(style); - let result = f(self); - self.borrow_mut().pop_text_style(); - result + if let Some(style) = style { + self.borrow_mut().push_text_style(style); + let result = f(self); + self.borrow_mut().pop_text_style(); + result + } else { + f(self) + } } fn set_global(&mut self, global: G) { diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 18b92c0b8b..92959ef057 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -2,7 +2,7 @@ use crate::{ black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Result, - Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext, + Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, }; use refineable::{Cascade, Refineable}; use smallvec::SmallVec; @@ -220,7 +220,7 @@ pub struct HighlightStyle { impl Eq for HighlightStyle {} impl Style { - pub fn text_style(&self, _cx: &WindowContext) -> Option<&TextStyleRefinement> { + pub fn text_style(&self) -> Option<&TextStyleRefinement> { if self.text.is_some() { Some(&self.text) } else { @@ -228,13 +228,47 @@ impl Style { } } - pub fn with_text_style(&self, cx: &mut C, f: F) -> R + pub fn overflow_mask(&self, bounds: Bounds) -> Option> { + match self.overflow { + Point { + x: Overflow::Visible, + y: Overflow::Visible, + } => None, + _ => { + let current_mask = bounds; + let min = current_mask.origin; + let max = current_mask.lower_right(); + let bounds = match ( + self.overflow.x == Overflow::Visible, + self.overflow.y == Overflow::Visible, + ) { + // x and y both visible + (true, true) => return None, + // x visible, y hidden + (true, false) => Bounds::from_corners( + point(min.x, bounds.origin.y), + point(max.x, bounds.lower_right().y), + ), + // x hidden, y visible + (false, true) => Bounds::from_corners( + point(bounds.origin.x, min.y), + point(bounds.lower_right().x, max.y), + ), + // both hidden + (false, false) => bounds, + }; + Some(ContentMask { bounds }) + } + } + } + + pub fn apply_text_style(&self, cx: &mut C, f: F) -> R where C: BorrowAppContext, F: FnOnce(&mut C) -> R, { if self.text.is_some() { - cx.with_text_style(self.text.clone(), f) + cx.with_text_style(Some(self.text.clone()), f) } else { f(cx) } @@ -274,7 +308,7 @@ impl Style { bounds: mask_bounds, }; - cx.with_content_mask(mask, f) + cx.with_content_mask(Some(mask), f) } /// Paints the background of an element styled with this style. diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 52c5464e4f..edda71f13f 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1067,7 +1067,7 @@ impl<'a> WindowContext<'a> { if let Some(active_drag) = self.app.active_drag.take() { self.with_z_index(1, |cx| { let offset = cx.mouse_position() - active_drag.cursor_offset; - cx.with_element_offset(Some(offset), |cx| { + cx.with_element_offset(offset, |cx| { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); active_drag.view.draw(available_space, cx); @@ -1076,7 +1076,7 @@ impl<'a> WindowContext<'a> { }); } else if let Some(active_tooltip) = self.app.active_tooltip.take() { self.with_z_index(1, |cx| { - cx.with_element_offset(Some(active_tooltip.cursor_offset), |cx| { + cx.with_element_offset(active_tooltip.cursor_offset, |cx| { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); active_tooltip.view.draw(available_space, cx); @@ -1553,43 +1553,26 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { /// with the current mask. fn with_content_mask( &mut self, - mask: ContentMask, + mask: Option>, f: impl FnOnce(&mut Self) -> R, ) -> R { - let mask = mask.intersect(&self.content_mask()); - self.window_mut() - .current_frame - .content_mask_stack - .push(mask); - let result = f(self); - self.window_mut().current_frame.content_mask_stack.pop(); - result + if let Some(mask) = mask { + let mask = mask.intersect(&self.content_mask()); + self.window_mut() + .current_frame + .content_mask_stack + .push(mask); + let result = f(self); + self.window_mut().current_frame.content_mask_stack.pop(); + result + } else { + f(self) + } } /// Update the global element offset based on the given offset. This is used to implement /// scrolling and position drag handles. fn with_element_offset( - &mut self, - offset: Option>, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - let Some(offset) = offset else { - return f(self); - }; - - let offset = self.element_offset() + offset; - self.window_mut() - .current_frame - .element_offset_stack - .push(offset); - let result = f(self); - self.window_mut().current_frame.element_offset_stack.pop(); - result - } - - /// Update the global element offset based on the given offset. This is used to implement - /// scrolling and position drag handles. - fn with_element_offset2( &mut self, offset: Point, f: impl FnOnce(&mut Self) -> R, From e0416e9d2a68800d9163b21b9d0c691617a87af6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 21:41:41 -0700 Subject: [PATCH 063/126] Fix SingleLine editor font size --- crates/editor2/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 0995bbd534..fe98dd8679 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9383,7 +9383,7 @@ impl Render for Editor { color: cx.theme().colors().text, font_family: "Zed Sans".into(), // todo!() font_features: FontFeatures::default(), - font_size: settings.ui_font_size.into(), + font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()), From 4ef95f05e8217ca43a06d18ece83aa4f028ce5e7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 21:42:04 -0700 Subject: [PATCH 064/126] Fix elevation on go_to_line2 --- crates/go_to_line2/src/go_to_line.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 50592901b5..1d57be6fd0 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -5,7 +5,7 @@ use gpui::{ }; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_stack, modal, v_stack, Label, LabelColor}; +use ui::{h_stack, v_stack, Label, LabelColor, StyledExt}; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::{Modal, ModalEvent, Workspace}; @@ -148,7 +148,8 @@ impl Render for GoToLine { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - modal(cx) + div() + .elevation_2(cx) .context("GoToLine") .on_action(Self::cancel) .on_action(Self::confirm) From ad017a5df52ebb465fabcde85031cbdac92950fc Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 21:42:27 -0700 Subject: [PATCH 065/126] Allow clicking on commands in the command palette --- crates/gpui2/src/interactive.rs | 40 +++++++++++++++--- crates/gpui2/src/window.rs | 6 +++ crates/picker2/src/picker2.rs | 56 ++++++++++++++++++++----- crates/ui2/src/components/keybinding.rs | 2 + crates/workspace2/src/modal_layer.rs | 25 ++++++----- 5 files changed, 104 insertions(+), 25 deletions(-) diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index aacaeac01f..312121c954 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -71,6 +71,40 @@ pub trait StatelessInteractive: Element { self } + fn on_any_mouse_down( + mut self, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.stateless_interactivity() + .mouse_down_listeners + .push(Box::new(move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + handler(view, event, cx) + } + })); + self + } + + fn on_any_mouse_up( + mut self, + handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.stateless_interactivity() + .mouse_up_listeners + .push(Box::new(move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + handler(view, event, cx) + } + })); + self + } + fn on_mouse_up( mut self, button: MouseButton, @@ -111,7 +145,6 @@ pub trait StatelessInteractive: Element { fn on_mouse_up_out( mut self, - button: MouseButton, handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, ) -> Self where @@ -120,10 +153,7 @@ pub trait StatelessInteractive: Element { self.stateless_interactivity() .mouse_up_listeners .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Capture - && event.button == button - && !bounds.contains_point(&event.position) - { + if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { handler(view, event, cx); } })); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 450b36ea81..eb69b451b3 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -101,6 +101,12 @@ pub struct FocusHandle { handles: Arc>>, } +impl std::fmt::Debug for FocusHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("FocusHandle({:?})", self.id)) + } +} + impl FocusHandle { pub(crate) fn new(handles: &Arc>>) -> Self { let id = handles.write().insert(AtomicUsize::new(1)); diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 836a612491..0cfe5c8992 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ - div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled, Task, - UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, + div, uniform_list, Component, Div, MouseButton, ParentElement, Render, StatelessInteractive, + Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; @@ -11,6 +11,7 @@ pub struct Picker { scroll_handle: UniformListScrollHandle, editor: View, pending_update_matches: Option>, + confirm_on_update: Option, } pub trait PickerDelegate: Sized + 'static { @@ -44,9 +45,10 @@ impl Picker { cx.subscribe(&editor, Self::on_input_editor_event).detach(); let mut this = Self { delegate, + editor, scroll_handle: UniformListScrollHandle::new(), pending_update_matches: None, - editor, + confirm_on_update: None, }; this.update_matches("".to_string(), cx); this @@ -101,11 +103,26 @@ impl Picker { } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - self.delegate.confirm(false, cx); + if self.pending_update_matches.is_some() { + self.confirm_on_update = Some(false) + } else { + self.delegate.confirm(false, cx); + } } fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { - self.delegate.confirm(true, cx); + if self.pending_update_matches.is_some() { + self.confirm_on_update = Some(true) + } else { + self.delegate.confirm(true, cx); + } + } + + fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext) { + cx.stop_propagation(); + cx.prevent_default(); + self.delegate.set_selected_index(ix, cx); + self.delegate.confirm(secondary, cx); } fn on_input_editor_event( @@ -136,6 +153,9 @@ impl Picker { let index = self.delegate.selected_index(); self.scroll_handle.scroll_to_item(index); self.pending_update_matches = None; + if let Some(secondary) = self.confirm_on_update.take() { + self.delegate.confirm(secondary, cx); + } cx.notify(); } } @@ -173,7 +193,22 @@ impl Render for Picker { let selected_ix = this.delegate.selected_index(); visible_range .map(|ix| { - this.delegate.render_match(ix, ix == selected_ix, cx) + div() + .on_mouse_down( + MouseButton::Left, + move |this: &mut Self, event, cx| { + this.handle_click( + ix, + event.modifiers.command, + cx, + ) + }, + ) + .child(this.delegate.render_match( + ix, + ix == selected_ix, + cx, + )) }) .collect() } @@ -186,10 +221,11 @@ impl Render for Picker { }) .when(self.delegate.match_count() == 0, |el| { el.child( - v_stack() - .p_1() - .grow() - .child(Label::new("No matches").color(LabelColor::Muted)), + v_stack().p_1().grow().child( + div() + .px_1() + .child(Label::new("No matches").color(LabelColor::Muted)), + ), ) }) } diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index b6c435c607..a3e5a870a6 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -32,6 +32,7 @@ impl KeyBinding { div() .flex() .gap_1() + .when(keystroke.modifiers.function, |el| el.child(Key::new("fn"))) .when(keystroke.modifiers.control, |el| el.child(Key::new("^"))) .when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥"))) .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘"))) @@ -136,6 +137,7 @@ mod stories { .child(KeyBinding::new(binding("a z"))) .child(Story::label(cx, "Chord with Modifier")) .child(KeyBinding::new(binding("ctrl-a shift-z"))) + .child(KeyBinding::new(binding("fn-s"))) } } } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 09ffa6c13f..b3a5de8fb2 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -2,7 +2,7 @@ use gpui::{ div, px, AnyView, Div, EventEmitter, FocusHandle, ParentElement, Render, StatelessInteractive, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; -use ui::v_stack; +use ui::{h_stack, v_stack}; pub struct ActiveModal { modal: AnyView, @@ -33,8 +33,6 @@ impl ModalLayer { V: Modal, B: FnOnce(&mut ViewContext) -> V, { - let previous_focus = cx.focused(); - if let Some(active_modal) = &self.active_modal { let is_close = active_modal.modal.clone().downcast::().is_ok(); self.hide_modal(cx); @@ -85,9 +83,6 @@ impl Render for ModalLayer { div() .absolute() - .flex() - .flex_col() - .items_center() .size_full() .top_0() .left_0() @@ -96,11 +91,21 @@ impl Render for ModalLayer { v_stack() .h(px(0.0)) .top_20() + .flex() + .flex_col() + .items_center() .track_focus(&active_modal.focus_handle) - .on_mouse_down_out(|this: &mut Self, event, cx| { - this.hide_modal(cx); - }) - .child(active_modal.modal.clone()), + .child( + h_stack() + // needed to prevent mouse events leaking to the + // UI below. // todo! for gpui3. + .on_any_mouse_down(|_, _, cx| cx.stop_propagation()) + .on_any_mouse_up(|_, _, cx| cx.stop_propagation()) + .on_mouse_down_out(|this: &mut Self, event, cx| { + this.hide_modal(cx); + }) + .child(active_modal.modal.clone()), + ), ) } } From 83a5f74493f349d4cc2c4b921d946445b4775223 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 22:02:05 -0700 Subject: [PATCH 066/126] Checkpoint --- crates/gpui2/src/elements/node.rs | 117 +++++++++++------------------- 1 file changed, 41 insertions(+), 76 deletions(-) diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index 57f825994f..b412038cad 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -547,17 +547,6 @@ impl InteractiveComponent for Node { } } -pub struct NodeState { - child_layout_ids: SmallVec<[LayoutId; 4]>, - interactive_state: InteractiveElementState, -} - -impl AsMut for InteractiveElementState { - fn as_mut(&mut self) -> &mut InteractiveElementState { - self - } -} - impl Element for Node { type ElementState = NodeState; @@ -660,26 +649,25 @@ impl Element for Node { } } -pub enum FocusStatus { - /// The current element is not focused, and does not contain or descend from the focused element. - None, - /// The current element is focused. - Focus, - /// The current element contains the focused element - FocusIn, - /// The current element descends from the focused element - InFocus, +pub struct NodeState { + child_layout_ids: SmallVec<[LayoutId; 4]>, + interactive_state: InteractiveElementState, +} + +impl AsMut for InteractiveElementState { + fn as_mut(&mut self) -> &mut InteractiveElementState { + self + } } pub struct Interactivity { - active: Option, - group_active: bool, hovered: bool, group_hovered: bool, - focus_status: FocusStatus, key_context: KeyContext, focus_handle: Option, + focusable: bool, scroll_offset: Point, + group: Option, base_style: StyleRefinement, focus_style: StyleRefinement, focus_in_style: StyleRefinement, @@ -690,8 +678,6 @@ pub struct Interactivity { group_active_style: Option, drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, - group: Option, - dispatch_context: KeyContext, mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, @@ -708,6 +694,7 @@ pub struct Interactivity { #[derive(Default)] pub struct InteractiveElementState { + focus_handle: Option, clicked_state: Arc>, hover_state: Arc>, pending_mouse_down: Arc>>, @@ -738,22 +725,26 @@ impl Interactivity where V: 'static, { - fn compute_style(&self, bounds: Option>, cx: &mut ViewContext) -> Style { + fn compute_style( + &self, + bounds: Option>, + element_state: &mut InteractiveElementState, + cx: &mut ViewContext, + ) -> Style { let mut style = Style::default(); style.refine(&self.base_style); - match self.focus_status { - FocusStatus::None => {} - FocusStatus::Focus => { + if let Some(focus_handle) = self.focus_handle.as_ref() { + if focus_handle.contains_focused(cx) { + style.refine(&self.focus_in_style); + } + + if focus_handle.within_focused(cx) { + style.refine(&self.in_focus_style); + } + + if focus_handle.is_focused(cx) { style.refine(&self.focus_style); - style.refine(&self.focus_in_style); - style.refine(&self.in_focus_style); - } - FocusStatus::FocusIn => { - style.refine(&self.focus_in_style); - } - FocusStatus::InFocus => { - style.refine(&self.in_focus_style); } } @@ -793,13 +784,14 @@ where } } - if self.group_active { + let clicked_state = element_state.clicked_state.lock(); + if clicked_state.group { if let Some(group) = self.group_active_style.as_ref() { style.refine(&group.style) } } - if self.active.is_some() { + if clicked_state.element { style.refine(&self.active_style) } @@ -812,34 +804,12 @@ where cx: &mut ViewContext, f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, ) -> LayoutId { - let mut style = Style::default(); - style.refine(&self.base_style); - - if let Some(focus_handle) = self.focus_handle.as_ref() { - if focus_handle.contains_focused(cx) { - style.refine(&self.focus_in_style); - } - - if focus_handle.within_focused(cx) { - style.refine(&self.in_focus_style); - } - - if focus_handle.is_focused(cx) { - style.refine(&self.focus_style); - } - } - - let clicked_state = element_state.clicked_state.lock(); - if clicked_state.group { - if let Some(group_style) = self.group_active_style.as_ref() { - style.refine(&group_style.style); - } - } - if clicked_state.element { - style.refine(&self.active_style); - } - - f(style, cx) + let style = self.compute_style(None, element_state, cx); + cx.with_key_dispatch( + self.key_context.clone(), + self.focus_handle.clone(), + |_, cx| f(style, cx), + ) } fn paint( @@ -850,7 +820,7 @@ where cx: &mut ViewContext, f: impl FnOnce(Style, Point, &mut ViewContext), ) { - let style = self.compute_style(Some(bounds), cx); + let style = self.compute_style(Some(bounds), element_state, cx); if let Some(mouse_cursor) = style.mouse_cursor { let hovered = bounds.contains_point(&cx.mouse_position()); @@ -935,8 +905,6 @@ where }); } - let mut element_state: &mut InteractiveElementState = element_state.as_mut(); - let click_listeners = mem::take(&mut self.click_listeners); let drag_listener = mem::take(&mut self.drag_listener); @@ -1136,14 +1104,12 @@ where impl Default for Interactivity { fn default() -> Self { Self { - active: None, - group_active: false, hovered: false, group_hovered: false, - focus_status: FocusStatus::None, key_context: KeyContext::default(), focus_handle: None, scroll_offset: Point::default(), + group: None, base_style: StyleRefinement::default(), focus_style: StyleRefinement::default(), focus_in_style: StyleRefinement::default(), @@ -1154,8 +1120,6 @@ impl Default for Interactivity { group_active_style: None, drag_over_styles: SmallVec::new(), group_drag_over_styles: SmallVec::new(), - group: None, - dispatch_context: KeyContext::default(), mouse_down_listeners: SmallVec::new(), mouse_up_listeners: SmallVec::new(), mouse_move_listeners: SmallVec::new(), @@ -1168,6 +1132,7 @@ impl Default for Interactivity { drag_listener: None, hover_listener: None, tooltip_builder: None, + focusable: todo!(), } } } @@ -1240,7 +1205,7 @@ where type ElementState = E::ElementState; fn id(&self) -> Option { - todo!() + self.element.id() } fn initialize( From f71afdb0f2226fae12039b097123867c781b257f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 22:22:09 -0700 Subject: [PATCH 067/126] Checkpoint --- crates/gpui2/src/elements/node.rs | 264 +++++++++++++++++------------- 1 file changed, 153 insertions(+), 111 deletions(-) diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index b412038cad..43bac764a9 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -557,17 +557,20 @@ impl Element for Node { fn initialize( &mut self, view_state: &mut V, - previous_element_state: Option, + element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - for child in &mut self.children { - child.initialize(view_state, cx); - } + let interactive_state = + self.interactivity + .initialize(element_state.map(|s| s.interactive_state), cx, |cx| { + for child in &mut self.children { + child.initialize(view_state, cx); + } + }); + NodeState { + interactive_state, child_layout_ids: SmallVec::new(), - interactive_state: previous_element_state - .map(|s| s.interactive_state) - .unwrap_or_default(), } } @@ -664,7 +667,7 @@ pub struct Interactivity { hovered: bool, group_hovered: bool, key_context: KeyContext, - focus_handle: Option, + tracked_focus_handle: Option, focusable: bool, scroll_offset: Point, group: Option, @@ -692,110 +695,29 @@ pub struct Interactivity { tooltip_builder: Option>, } -#[derive(Default)] -pub struct InteractiveElementState { - focus_handle: Option, - clicked_state: Arc>, - hover_state: Arc>, - pending_mouse_down: Arc>>, - scroll_offset: Option>>>, - active_tooltip: Arc>>, -} - -struct ActiveTooltip { - #[allow(unused)] // used to drop the task - waiting: Option>, - tooltip: Option, -} - -/// Whether or not the element or a group that contains it is clicked by the mouse. -#[derive(Copy, Clone, Default, Eq, PartialEq)] -struct ElementClickedState { - pub group: bool, - pub element: bool, -} - -impl ElementClickedState { - fn is_clicked(&self) -> bool { - self.group || self.element - } -} - impl Interactivity where V: 'static, { - fn compute_style( - &self, - bounds: Option>, - element_state: &mut InteractiveElementState, + fn initialize( + &mut self, + element_state: Option, cx: &mut ViewContext, - ) -> Style { - let mut style = Style::default(); - style.refine(&self.base_style); - - if let Some(focus_handle) = self.focus_handle.as_ref() { - if focus_handle.contains_focused(cx) { - style.refine(&self.focus_in_style); - } - - if focus_handle.within_focused(cx) { - style.refine(&self.in_focus_style); - } - - if focus_handle.is_focused(cx) { - style.refine(&self.focus_style); - } + f: impl FnOnce(&mut ViewContext), + ) -> InteractiveElementState { + let mut element_state = element_state.unwrap_or_default(); + // Ensure we store a focus handle in our element state if we're focusable. + // If there's an explicit focus handle we're tracking, use that. Otherwise + // create a new handle and store it in the element state, which lives for as + // as frames contain an element with this id. + if self.focusable { + element_state.focus_handle.get_or_insert_with(|| { + self.tracked_focus_handle + .clone() + .unwrap_or_else(|| cx.focus_handle()) + }); } - - if let Some(bounds) = bounds { - let mouse_position = cx.mouse_position(); - if let Some(group_hover) = self.group_hover_style.as_ref() { - if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { - if group_bounds.contains_point(&mouse_position) { - style.refine(&group_hover.style); - } - } - } - if bounds.contains_point(&mouse_position) { - style.refine(&self.hover_style); - } - - if let Some(drag) = cx.active_drag.take() { - for (state_type, group_drag_style) in &self.group_drag_over_styles { - if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { - if *state_type == drag.view.entity_type() - && group_bounds.contains_point(&mouse_position) - { - style.refine(&group_drag_style.style); - } - } - } - - for (state_type, drag_over_style) in &self.drag_over_styles { - if *state_type == drag.view.entity_type() - && bounds.contains_point(&mouse_position) - { - style.refine(drag_over_style); - } - } - - cx.active_drag = Some(drag); - } - } - - let clicked_state = element_state.clicked_state.lock(); - if clicked_state.group { - if let Some(group) = self.group_active_style.as_ref() { - style.refine(&group.style) - } - } - - if clicked_state.element { - style.refine(&self.active_style) - } - - style + element_state } fn layout( @@ -807,7 +729,7 @@ where let style = self.compute_style(None, element_state, cx); cx.with_key_dispatch( self.key_context.clone(), - self.focus_handle.clone(), + self.tracked_focus_handle.clone(), |_, cx| f(style, cx), ) } @@ -1091,14 +1013,105 @@ where cx.with_key_dispatch( self.key_context.clone(), - self.focus_handle.clone(), - |_, cx| f(style, self.scroll_offset, cx), + self.tracked_focus_handle.clone(), + |_, cx| { + for listener in self.key_down_listeners.drain(..) { + cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { + listener(state, event, phase, cx); + }) + } + + for listener in self.key_up_listeners.drain(..) { + cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { + listener(state, event, phase, cx); + }) + } + + for (action_type, listener) in self.action_listeners.drain(..) { + cx.on_action(action_type, listener) + } + + f(style, self.scroll_offset, cx) + }, ); if let Some(group) = self.group.as_ref() { GroupBounds::pop(group, cx); } } + + fn compute_style( + &self, + bounds: Option>, + element_state: &mut InteractiveElementState, + cx: &mut ViewContext, + ) -> Style { + let mut style = Style::default(); + style.refine(&self.base_style); + + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + if focus_handle.contains_focused(cx) { + style.refine(&self.focus_in_style); + } + + if focus_handle.within_focused(cx) { + style.refine(&self.in_focus_style); + } + + if focus_handle.is_focused(cx) { + style.refine(&self.focus_style); + } + } + + if let Some(bounds) = bounds { + let mouse_position = cx.mouse_position(); + if let Some(group_hover) = self.group_hover_style.as_ref() { + if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { + if group_bounds.contains_point(&mouse_position) { + style.refine(&group_hover.style); + } + } + } + if bounds.contains_point(&mouse_position) { + style.refine(&self.hover_style); + } + + if let Some(drag) = cx.active_drag.take() { + for (state_type, group_drag_style) in &self.group_drag_over_styles { + if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { + if *state_type == drag.view.entity_type() + && group_bounds.contains_point(&mouse_position) + { + style.refine(&group_drag_style.style); + } + } + } + + for (state_type, drag_over_style) in &self.drag_over_styles { + if *state_type == drag.view.entity_type() + && bounds.contains_point(&mouse_position) + { + style.refine(drag_over_style); + } + } + + cx.active_drag = Some(drag); + } + } + + let clicked_state = element_state.clicked_state.lock(); + if clicked_state.group { + if let Some(group) = self.group_active_style.as_ref() { + style.refine(&group.style) + } + } + + if clicked_state.element { + style.refine(&self.active_style) + } + + style + } } impl Default for Interactivity { @@ -1107,7 +1120,7 @@ impl Default for Interactivity { hovered: false, group_hovered: false, key_context: KeyContext::default(), - focus_handle: None, + tracked_focus_handle: None, scroll_offset: Point::default(), group: None, base_style: StyleRefinement::default(), @@ -1132,11 +1145,40 @@ impl Default for Interactivity { drag_listener: None, hover_listener: None, tooltip_builder: None, - focusable: todo!(), + focusable: false, } } } +#[derive(Default)] +pub struct InteractiveElementState { + focus_handle: Option, + clicked_state: Arc>, + hover_state: Arc>, + pending_mouse_down: Arc>>, + scroll_offset: Option>>>, + active_tooltip: Arc>>, +} + +struct ActiveTooltip { + #[allow(unused)] // used to drop the task + waiting: Option>, + tooltip: Option, +} + +/// Whether or not the element or a group that contains it is clicked by the mouse. +#[derive(Copy, Clone, Default, Eq, PartialEq)] +struct ElementClickedState { + pub group: bool, + pub element: bool, +} + +impl ElementClickedState { + fn is_clicked(&self) -> bool { + self.group || self.element + } +} + #[derive(Default)] pub struct GroupBounds(HashMap; 1]>>); From 54a817a5abd7655910f7c4e9f57e0d78ba14b7a3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 22:28:33 -0700 Subject: [PATCH 068/126] Checkpoint --- crates/gpui2/src/elements/node.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index 43bac764a9..d5bfd63647 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -603,8 +603,6 @@ impl Element for Node { element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - let mut interactivity = mem::take(&mut self.interactivity); - let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); let content_size = if element_state.child_layout_ids.is_empty() { @@ -618,6 +616,7 @@ impl Element for Node { (child_max - child_min).into() }; + let mut interactivity = mem::take(&mut self.interactivity); interactivity.paint( bounds, content_size, @@ -664,8 +663,6 @@ impl AsMut for InteractiveElementState { } pub struct Interactivity { - hovered: bool, - group_hovered: bool, key_context: KeyContext, tracked_focus_handle: Option, focusable: bool, @@ -1117,8 +1114,6 @@ where impl Default for Interactivity { fn default() -> Self { Self { - hovered: false, - group_hovered: false, key_context: KeyContext::default(), tracked_focus_handle: None, scroll_offset: Point::default(), @@ -1256,7 +1251,7 @@ where element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - todo!() + self.element.initialize(view_state, element_state, cx) } fn layout( @@ -1265,7 +1260,7 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - todo!() + self.element.layout(view_state, element_state, cx) } fn paint( @@ -1275,7 +1270,7 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - todo!() + self.element.paint(bounds, view_state, element_state, cx); } } @@ -1314,10 +1309,10 @@ where V: 'static, E: Element, { - type ElementState = InteractiveElementState; + type ElementState = E::ElementState; fn id(&self) -> Option { - todo!() + self.element.id() } fn initialize( @@ -1326,7 +1321,7 @@ where element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - todo!() + self.element.initialize(view_state, element_state, cx) } fn layout( @@ -1335,7 +1330,7 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - todo!() + self.element.layout(view_state, element_state, cx) } fn paint( @@ -1345,6 +1340,6 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - todo!() + self.element.paint(bounds, view_state, element_state, cx) } } From 4a3a1ad0c35ff622eb8c966e984652f4f0968db2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 22:42:19 -0700 Subject: [PATCH 069/126] Checkpoint --- crates/editor2/src/element.rs | 2 +- crates/gpui2/src/elements/div.rs | 40 +++++++++++-------------------- crates/gpui2/src/elements/node.rs | 2 ++ crates/gpui2/src/view.rs | 6 ++--- crates/gpui2/src/window.rs | 25 +++++++++++-------- 5 files changed, 35 insertions(+), 40 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 630b062d1f..8ee99e19af 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2455,7 +2455,7 @@ impl Element for EditorElement { editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. let dispatch_context = editor.dispatch_context(cx); - cx.with_element_id(cx.view().entity_id(), |global_id, cx| { + cx.with_element_id(Some(cx.view().entity_id()), |cx| { cx.with_key_dispatch( dispatch_context, Some(editor.focus_handle.clone()), diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 2b411f7609..1d85450c29 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use crate::{ point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, ElementInteractivity, - FocusHandle, FocusListeners, Focusable, FocusableKeyDispatch, GlobalElementId, GroupBounds, + FocusHandle, FocusListeners, Focusable, FocusableKeyDispatch, GroupBounds, InteractiveElementState, KeyContext, KeyDispatch, LayoutId, NonFocusableKeyDispatch, Overflow, ParentElement, Pixels, Point, SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity, Style, StyleRefinement, Styled, ViewContext, @@ -93,18 +93,6 @@ where self } - fn with_element_id( - &mut self, - cx: &mut ViewContext, - f: impl FnOnce(&mut Self, Option, &mut ViewContext) -> R, - ) -> R { - if let Some(id) = self.id() { - cx.with_element_id(id, |global_id, cx| f(self, Some(global_id), cx)) - } else { - f(self, None, cx) - } - } - pub fn compute_style( &self, bounds: Bounds, @@ -229,14 +217,14 @@ where cx: &mut ViewContext, ) -> Self::ElementState { let mut element_state = element_state.unwrap_or_default(); - self.with_element_id(cx, |this, _global_id, cx| { - this.key_dispatch.initialize( + cx.with_element_id(self.id(), |cx| { + self.key_dispatch.initialize( element_state.focus_handle.take(), cx, |focus_handle, cx| { - this.interactivity.initialize(cx); + self.interactivity.initialize(cx); element_state.focus_handle = focus_handle; - for child in &mut this.children { + for child in &mut self.children { child.initialize(view_state, cx); } }, @@ -253,8 +241,8 @@ where ) -> LayoutId { let style = self.compute_style(Bounds::default(), element_state, cx); style.apply_text_style(cx, |cx| { - self.with_element_id(cx, |this, _global_id, cx| { - let layout_ids = this + cx.with_element_id(self.id(), |cx| { + let layout_ids = self .children .iter_mut() .map(|child| child.layout(view_state, cx)) @@ -272,8 +260,8 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - self.with_element_id(cx, |this, _global_id, cx| { - let style = this.compute_style(bounds, element_state, cx); + cx.with_element_id(self.id(), |cx| { + let style = self.compute_style(bounds, element_state, cx); if style.visibility == Visibility::Hidden { return; } @@ -285,7 +273,7 @@ where } } - if let Some(group) = this.group.clone() { + if let Some(group) = self.group.clone() { GroupBounds::push(group, bounds, cx); } @@ -308,8 +296,8 @@ where cx.with_z_index(z_index, |cx| { cx.with_z_index(0, |cx| { style.paint(bounds, cx); - this.key_dispatch.paint(bounds, cx); - this.interactivity.handle_events( + self.key_dispatch.paint(bounds, cx); + self.interactivity.handle_events( bounds, content_size, style.overflow, @@ -322,7 +310,7 @@ where style.apply_overflow(bounds, cx, |cx| { let scroll_offset = element_state.interactive.scroll_offset(); cx.with_element_offset(scroll_offset.unwrap_or_default(), |cx| { - for child in &mut this.children { + for child in &mut self.children { child.paint(view_state, cx); } }); @@ -331,7 +319,7 @@ where }); }); - if let Some(group) = this.group.as_ref() { + if let Some(group) = self.group.as_ref() { GroupBounds::pop(group, cx); } }) diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index d5bfd63647..f3d2b38dfb 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -1008,6 +1008,8 @@ where GroupBounds::push(group, bounds, cx); } + todo!(); + // cx.with_element_id(self.i, f); cx.with_key_dispatch( self.key_context.clone(), self.tracked_focus_handle.clone(), diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index d12d84f43b..3299ce7d45 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -286,7 +286,7 @@ mod any_view { use std::any::Any; pub(crate) fn initialize(view: &AnyView, cx: &mut WindowContext) -> Box { - cx.with_element_id(view.model.entity_id, |_, cx| { + cx.with_element_id(Some(view.model.entity_id), |cx| { let view = view.clone().downcast::().unwrap(); let element = view.update(cx, |view, cx| { let mut element = AnyElement::new(view.render(cx)); @@ -302,7 +302,7 @@ mod any_view { element: &mut Box, cx: &mut WindowContext, ) -> LayoutId { - cx.with_element_id(view.model.entity_id, |_, cx| { + cx.with_element_id(Some(view.model.entity_id), |cx| { let view = view.clone().downcast::().unwrap(); let element = element.downcast_mut::>().unwrap(); view.update(cx, |view, cx| element.layout(view, cx)) @@ -314,7 +314,7 @@ mod any_view { element: &mut Box, cx: &mut WindowContext, ) { - cx.with_element_id(view.model.entity_id, |_, cx| { + cx.with_element_id(Some(view.model.entity_id), |cx| { let view = view.clone().downcast::().unwrap(); let element = element.downcast_mut::>().unwrap(); view.update(cx, |view, cx| element.paint(view, cx)) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index edda71f13f..c5d794aa3d 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1537,16 +1537,19 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { /// used to associate state with identified elements across separate frames. fn with_element_id( &mut self, - id: impl Into, - f: impl FnOnce(GlobalElementId, &mut Self) -> R, + id: Option>, + f: impl FnOnce(&mut Self) -> R, ) -> R { - let window = self.window_mut(); - window.element_id_stack.push(id.into()); - let global_id = window.element_id_stack.clone(); - let result = f(global_id, self); - let window: &mut Window = self.borrow_mut(); - window.element_id_stack.pop(); - result + if let Some(id) = id.map(Into::into) { + let window = self.window_mut(); + window.element_id_stack.push(id.into()); + let result = f(self); + let window: &mut Window = self.borrow_mut(); + window.element_id_stack.pop(); + result + } else { + f(self) + } } /// Invoke the given function with the given content mask after intersecting it @@ -1613,7 +1616,9 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { where S: 'static, { - self.with_element_id(id, |global_id, cx| { + self.with_element_id(Some(id), |cx| { + let global_id = cx.window().element_id_stack.clone(); + if let Some(any) = cx .window_mut() .current_frame From 16683307645a355c95df6b39ac6ab87c3c73242e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 22:51:44 -0700 Subject: [PATCH 070/126] Checkpoint --- crates/gpui2/src/elements/node.rs | 89 +++++++++++++++---------------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index f3d2b38dfb..7129220e6a 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -1,6 +1,6 @@ use crate::{ point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, - BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, FocusHandle, KeyContext, + BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility, @@ -550,8 +550,8 @@ impl InteractiveComponent for Node { impl Element for Node { type ElementState = NodeState; - fn id(&self) -> Option { - None + fn id(&self) -> Option { + self.interactivity.element_id.clone() } fn initialize( @@ -560,13 +560,12 @@ impl Element for Node { element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - let interactive_state = - self.interactivity - .initialize(element_state.map(|s| s.interactive_state), cx, |cx| { - for child in &mut self.children { - child.initialize(view_state, cx); - } - }); + let interactive_state = self + .interactivity + .initialize(element_state.map(|s| s.interactive_state), cx); + for child in &mut self.children { + child.initialize(view_state, cx); + } NodeState { interactive_state, @@ -656,13 +655,8 @@ pub struct NodeState { interactive_state: InteractiveElementState, } -impl AsMut for InteractiveElementState { - fn as_mut(&mut self) -> &mut InteractiveElementState { - self - } -} - pub struct Interactivity { + element_id: Option, key_context: KeyContext, tracked_focus_handle: Option, focusable: bool, @@ -700,9 +694,9 @@ where &mut self, element_state: Option, cx: &mut ViewContext, - f: impl FnOnce(&mut ViewContext), ) -> InteractiveElementState { let mut element_state = element_state.unwrap_or_default(); + // Ensure we store a focus handle in our element state if we're focusable. // If there's an explicit focus handle we're tracking, use that. Otherwise // create a new handle and store it in the element state, which lives for as @@ -724,11 +718,13 @@ where f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, ) -> LayoutId { let style = self.compute_style(None, element_state, cx); - cx.with_key_dispatch( - self.key_context.clone(), - self.tracked_focus_handle.clone(), - |_, cx| f(style, cx), - ) + cx.with_element_id(self.element_id.clone(), |cx| { + cx.with_key_dispatch( + self.key_context.clone(), + self.tracked_focus_handle.clone(), + |_, cx| f(style, cx), + ) + }) } fn paint( @@ -1008,31 +1004,31 @@ where GroupBounds::push(group, bounds, cx); } - todo!(); - // cx.with_element_id(self.i, f); - cx.with_key_dispatch( - self.key_context.clone(), - self.tracked_focus_handle.clone(), - |_, cx| { - for listener in self.key_down_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { - listener(state, event, phase, cx); - }) - } + cx.with_element_id(self.element_id.clone(), |cx| { + cx.with_key_dispatch( + self.key_context.clone(), + self.tracked_focus_handle.clone(), + |_, cx| { + for listener in self.key_down_listeners.drain(..) { + cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { + listener(state, event, phase, cx); + }) + } - for listener in self.key_up_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { - listener(state, event, phase, cx); - }) - } + for listener in self.key_up_listeners.drain(..) { + cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { + listener(state, event, phase, cx); + }) + } - for (action_type, listener) in self.action_listeners.drain(..) { - cx.on_action(action_type, listener) - } + for (action_type, listener) in self.action_listeners.drain(..) { + cx.on_action(action_type, listener) + } - f(style, self.scroll_offset, cx) - }, - ); + f(style, self.scroll_offset, cx) + }, + ); + }); if let Some(group) = self.group.as_ref() { GroupBounds::pop(group, cx); @@ -1116,6 +1112,7 @@ where impl Default for Interactivity { fn default() -> Self { Self { + element_id: None, key_context: KeyContext::default(), tracked_focus_handle: None, scroll_offset: Point::default(), @@ -1243,7 +1240,7 @@ where { type ElementState = E::ElementState; - fn id(&self) -> Option { + fn id(&self) -> Option { self.element.id() } @@ -1313,7 +1310,7 @@ where { type ElementState = E::ElementState; - fn id(&self) -> Option { + fn id(&self) -> Option { self.element.id() } From 9382a304c4fe4404920905ff7436cf72e1a0620a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 23:03:14 -0700 Subject: [PATCH 071/126] Checkpoint --- crates/gpui2/src/elements/node.rs | 94 +++++++++++++----------------- crates/ui2/src/components/modal.rs | 2 +- 2 files changed, 41 insertions(+), 55 deletions(-) diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index 7129220e6a..f69ad25ab0 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -1,9 +1,9 @@ use crate::{ point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, - BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusHandle, KeyContext, - KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, - Task, View, ViewContext, Visibility, + BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, + KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, + StyleRefinement, Styled, Task, View, ViewContext, Visibility, }; use collections::HashMap; use parking_lot::Mutex; @@ -371,14 +371,12 @@ pub trait StatefulInteractiveComponent>: InteractiveCo } } -pub trait FocusableComponent { - fn focusability(&mut self) -> &mut Focusability; - +pub trait FocusableComponent: InteractiveComponent { fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, { - self.focusability().focus_style = f(StyleRefinement::default()); + self.interactivity().focus_style = f(StyleRefinement::default()); self } @@ -386,7 +384,7 @@ pub trait FocusableComponent { where Self: Sized, { - self.focusability().focus_in_style = f(StyleRefinement::default()); + self.interactivity().focus_in_style = f(StyleRefinement::default()); self } @@ -394,7 +392,7 @@ pub trait FocusableComponent { where Self: Sized, { - self.focusability().in_focus_style = f(StyleRefinement::default()); + self.interactivity().in_focus_style = f(StyleRefinement::default()); self } @@ -405,13 +403,13 @@ pub trait FocusableComponent { where Self: Sized, { - self.focusability() - .focus_listeners - .push(Box::new(move |view, focus_handle, event, cx| { + self.interactivity().focus_listeners.push(Box::new( + move |view, focus_handle, event, cx| { if event.focused.as_ref() == Some(focus_handle) { listener(view, event, cx) } - })); + }, + )); self } @@ -422,13 +420,13 @@ pub trait FocusableComponent { where Self: Sized, { - self.focusability() - .focus_listeners - .push(Box::new(move |view, focus_handle, event, cx| { + self.interactivity().focus_listeners.push(Box::new( + move |view, focus_handle, event, cx| { if event.blurred.as_ref() == Some(focus_handle) { listener(view, event, cx) } - })); + }, + )); self } @@ -439,9 +437,8 @@ pub trait FocusableComponent { where Self: Sized, { - self.focusability() - .focus_listeners - .push(Box::new(move |view, focus_handle, event, cx| { + self.interactivity().focus_listeners.push(Box::new( + move |view, focus_handle, event, cx| { let descendant_blurred = event .blurred .as_ref() @@ -454,7 +451,8 @@ pub trait FocusableComponent { if !descendant_blurred && descendant_focused { listener(view, event, cx) } - })); + }, + )); self } @@ -465,9 +463,8 @@ pub trait FocusableComponent { where Self: Sized, { - self.focusability() - .focus_listeners - .push(Box::new(move |view, focus_handle, event, cx| { + self.interactivity().focus_listeners.push(Box::new( + move |view, focus_handle, event, cx| { let descendant_blurred = event .blurred .as_ref() @@ -479,7 +476,8 @@ pub trait FocusableComponent { if descendant_blurred && !descendant_focused { listener(view, event, cx) } - })); + }, + )); self } } @@ -525,11 +523,6 @@ pub type KeyUpListener = pub type ActionListener = Box) + 'static>; -pub struct FocusEvent { - pub blurred: Option, - pub focused: Option, -} - pub struct Node { interactivity: Interactivity, children: Vec>, @@ -658,8 +651,9 @@ pub struct NodeState { pub struct Interactivity { element_id: Option, key_context: KeyContext, - tracked_focus_handle: Option, focusable: bool, + tracked_focus_handle: Option, + focus_listeners: FocusListeners, scroll_offset: Point, group: Option, base_style: StyleRefinement, @@ -1007,7 +1001,7 @@ where cx.with_element_id(self.element_id.clone(), |cx| { cx.with_key_dispatch( self.key_context.clone(), - self.tracked_focus_handle.clone(), + element_state.focus_handle.clone(), |_, cx| { for listener in self.key_down_listeners.drain(..) { cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { @@ -1025,6 +1019,15 @@ where cx.on_action(action_type, listener) } + if let Some(focus_handle) = element_state.focus_handle.as_ref() { + for listener in self.focus_listeners.drain(..) { + let focus_handle = focus_handle.clone(); + cx.on_focus_changed(move |view, event, cx| { + listener(view, &focus_handle, event, cx) + }); + } + } + f(style, self.scroll_offset, cx) }, ); @@ -1114,7 +1117,9 @@ impl Default for Interactivity { Self { element_id: None, key_context: KeyContext::default(), + focusable: false, tracked_focus_handle: None, + focus_listeners: SmallVec::default(), scroll_offset: Point::default(), group: None, base_style: StyleRefinement::default(), @@ -1139,7 +1144,6 @@ impl Default for Interactivity { drag_listener: None, hover_listener: None, tooltip_builder: None, - focusable: false, } } } @@ -1199,24 +1203,11 @@ impl GroupBounds { } pub struct Focusable { - focusability: Focusability, view_type: PhantomData, element: E, } -pub struct Focusability { - focus_handle: Option, - focus_listeners: FocusListeners, - focus_style: StyleRefinement, - focus_in_style: StyleRefinement, - in_focus_style: StyleRefinement, -} - -impl FocusableComponent for Focusable { - fn focusability(&mut self) -> &mut Focusability { - &mut self.focusability - } -} +impl> FocusableComponent for Focusable {} impl InteractiveComponent for Focusable where @@ -1274,7 +1265,6 @@ where } pub struct Stateful { - id: SharedString, view_type: PhantomData, element: E, } @@ -1297,11 +1287,7 @@ where } } -impl> FocusableComponent for Stateful { - fn focusability(&mut self) -> &mut Focusability { - self.element.focusability() - } -} +impl> FocusableComponent for Stateful {} impl Element for Stateful where diff --git a/crates/ui2/src/components/modal.rs b/crates/ui2/src/components/modal.rs index 805bbe95b2..75528b5c34 100644 --- a/crates/ui2/src/components/modal.rs +++ b/crates/ui2/src/components/modal.rs @@ -1,4 +1,4 @@ -use gpui::{AnyElement, Pixels}; +use gpui::AnyElement; use smallvec::SmallVec; use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label}; From ec16e70336552255adf99671ca4d3c4e3d1b5c5d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 13 Nov 2023 17:34:12 -0800 Subject: [PATCH 072/126] Sketch in core zed actions code --- crates/Cargo.toml | 38 ---- crates/workspace2/src/workspace2.rs | 5 +- crates/zed2/src/main.rs | 7 +- crates/zed2/src/zed2.rs | 257 ++++++++++++++++++++++++++++ crates/zed_actions2/src/lib.rs | 9 + 5 files changed, 274 insertions(+), 42 deletions(-) delete mode 100644 crates/Cargo.toml diff --git a/crates/Cargo.toml b/crates/Cargo.toml deleted file mode 100644 index fb49a4b515..0000000000 --- a/crates/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "ai" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/ai.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -gpui = { path = "../gpui" } -util = { path = "../util" } -language = { path = "../language" } -async-trait.workspace = true -anyhow.workspace = true -futures.workspace = true -lazy_static.workspace = true -ordered-float.workspace = true -parking_lot.workspace = true -isahc.workspace = true -regex.workspace = true -serde.workspace = true -serde_json.workspace = true -postage.workspace = true -rand.workspace = true -log.workspace = true -parse_duration = "2.1.1" -tiktoken-rs = "0.5.0" -matrixmultiply = "0.3.7" -rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } -bincode = "1.3.3" - -[dev-dependencies] -gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 2ddd210c9f..f5cbc6e787 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -72,7 +72,7 @@ pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use ui::{h_stack, Label}; use util::ResultExt; use uuid::Uuid; -use workspace_settings::{AutosaveSetting, WorkspaceSettings}; +pub use workspace_settings::{AutosaveSetting, WorkspaceSettings}; lazy_static! { static ref ZED_WINDOW_SIZE: Option> = env::var("ZED_WINDOW_SIZE") @@ -3506,13 +3506,14 @@ impl Workspace { pub fn register_action( &mut self, callback: impl Fn(&mut Self, &A, &mut ViewContext) + 'static, - ) { + ) -> &mut Self { let callback = Arc::new(callback); self.workspace_actions.push(Box::new(move |div| { let callback = callback.clone(); div.on_action(move |workspace, event, cx| (callback.clone())(workspace, event, cx)) })); + self } fn add_workspace_actions_listeners( diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 2deaff2149..9a4ad81806 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -50,14 +50,16 @@ use util::{ use uuid::Uuid; use workspace::{AppState, WorkspaceStore}; use zed2::{ - build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace, - languages, Assets, IsOnlyInstance, OpenListener, OpenRequest, + build_window_options, ensure_only_instance, handle_cli_connection, init_zed_actions, + initialize_workspace, languages, Assets, IsOnlyInstance, OpenListener, OpenRequest, }; mod open_listener; fn main() { menu::init(); + zed_actions::init(); + let http = http::client(); init_paths(); init_logger(); @@ -209,6 +211,7 @@ fn main() { // zed::init(&app_state, cx); // cx.set_menus(menus::menus()); + init_zed_actions(cx); if stdout_is_a_pty() { cx.activate(true); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index de985496c8..54723ee8d8 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -15,6 +15,7 @@ pub use only_instance::*; pub use open_listener::*; use anyhow::Result; +use settings::Settings; use std::sync::Arc; use uuid::Uuid; use workspace::{AppState, Workspace}; @@ -47,6 +48,214 @@ pub fn build_window_options( } } +pub fn init_zed_actions(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, cx| { + workspace + // cx.add_action(about); + // cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| { + // cx.platform().hide(); + // }); + // cx.add_global_action(|_: &HideOthers, cx: &mut gpui::AppContext| { + // cx.platform().hide_other_apps(); + // }); + // cx.add_global_action(|_: &ShowAll, cx: &mut gpui::AppContext| { + // cx.platform().unhide_other_apps(); + // }); + // cx.add_action( + // |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext| { + // cx.minimize_window(); + // }, + // ); + // cx.add_action( + // |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext| { + // cx.zoom_window(); + // }, + // ); + // cx.add_action( + // |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext| { + // cx.toggle_full_screen(); + // }, + // ); + .register_action(|workspace, _: &zed_actions::Quit, cx| quit(cx)); + // cx.add_global_action(move |action: &OpenZedURL, cx| { + // cx.global::>() + // .open_urls(vec![action.url.clone()]) + // }); + // cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); + // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { + // theme::adjust_font_size(cx, |size| *size += 1.0) + // }); + // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| { + // theme::adjust_font_size(cx, |size| *size -= 1.0) + // }); + // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx)); + // cx.add_global_action(move |_: &install_cli::Install, cx| { + // cx.spawn(|cx| async move { + // install_cli::install_cli(&cx) + // .await + // .context("error creating CLI symlink") + // }) + // .detach_and_log_err(cx); + // }); + // cx.add_action( + // move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext| { + // open_log_file(workspace, cx); + // }, + // ); + // cx.add_action( + // move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext| { + // open_bundled_file( + // workspace, + // asset_str::("licenses.md"), + // "Open Source License Attribution", + // "Markdown", + // cx, + // ); + // }, + // ); + // cx.add_action( + // move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext| { + // open_telemetry_log_file(workspace, cx); + // }, + // ); + // cx.add_action( + // move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { + // create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx); + // }, + // ); + // cx.add_action( + // move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + // create_and_open_local_file(&paths::SETTINGS, cx, || { + // settings::initial_user_settings_content().as_ref().into() + // }) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action(open_local_settings_file); + // cx.add_action( + // move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { + // open_bundled_file( + // workspace, + // settings::default_keymap(), + // "Default Key Bindings", + // "JSON", + // cx, + // ); + // }, + // ); + // cx.add_action( + // move |workspace: &mut Workspace, + // _: &OpenDefaultSettings, + // cx: &mut ViewContext| { + // open_bundled_file( + // workspace, + // settings::default_settings(), + // "Default Settings", + // "JSON", + // cx, + // ); + // }, + // ); + // cx.add_action({ + // move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { + // let app_state = workspace.app_state().clone(); + // let markdown = app_state.languages.language_for_name("JSON"); + // let window = cx.window(); + // cx.spawn(|workspace, mut cx| async move { + // let markdown = markdown.await.log_err(); + // let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| { + // anyhow!("could not debug elements for window {}", window.id()) + // })?) + // .unwrap(); + // workspace + // .update(&mut cx, |workspace, cx| { + // workspace.with_local_workspace(cx, move |workspace, cx| { + // let project = workspace.project().clone(); + + // let buffer = project + // .update(cx, |project, cx| { + // project.create_buffer(&content, markdown, cx) + // }) + // .expect("creating buffers on a local workspace always succeeds"); + // let buffer = cx.add_model(|cx| { + // MultiBuffer::singleton(buffer, cx) + // .with_title("Debug Elements".into()) + // }); + // workspace.add_item( + // Box::new(cx.add_view(|cx| { + // Editor::for_multibuffer(buffer, Some(project.clone()), cx) + // })), + // cx, + // ); + // }) + // })? + // .await + // }) + // .detach_and_log_err(cx); + // } + // }); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &project_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::collab_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::chat_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::notification_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &terminal_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_global_action({ + // let app_state = Arc::downgrade(&app_state); + // move |_: &NewWindow, cx: &mut AppContext| { + // if let Some(app_state) = app_state.upgrade() { + // open_new(&app_state, cx, |workspace, cx| { + // Editor::new_file(workspace, &Default::default(), cx) + // }) + // .detach(); + // } + // } + // }); + // cx.add_global_action({ + // let app_state = Arc::downgrade(&app_state); + // move |_: &NewFile, cx: &mut AppContext| { + // if let Some(app_state) = app_state.upgrade() { + // open_new(&app_state, cx, |workspace, cx| { + // Editor::new_file(workspace, &Default::default(), cx) + // }) + // .detach(); + // } + // } + // }); + // load_default_keymap(cx); + }) + .detach(); +} + pub fn initialize_workspace( workspace_handle: WeakView, was_deserialized: bool, @@ -205,3 +414,51 @@ pub fn initialize_workspace( Ok(()) }) } + +fn quit(cx: &mut gpui::AppContext) { + let should_confirm = workspace::WorkspaceSettings::get_global(cx).confirm_quit; + cx.spawn(|mut cx| async move { + // let mut workspace_windows = cx + // .windows() + // .into_iter() + // .filter_map(|window| window.downcast::()) + // .collect::>(); + + // // If multiple windows have unsaved changes, and need a save prompt, + // // prompt in the active window before switching to a different window. + // workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + + // if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { + // let answer = window.prompt( + // PromptLevel::Info, + // "Are you sure you want to quit?", + // &["Quit", "Cancel"], + // &mut cx, + // ); + + // if let Some(mut answer) = answer { + // let answer = answer.next().await; + // if answer != Some(0) { + // return Ok(()); + // } + // } + // } + + // // If the user cancels any save prompt, then keep the app open. + // for window in workspace_windows { + // if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { + // workspace.prepare_to_close(true, cx) + // }) { + // if !should_close.await? { + // return Ok(()); + // } + // } + // } + cx.update(|cx| { + cx.quit(); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); +} diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs index 090352b2cc..097766492f 100644 --- a/crates/zed_actions2/src/lib.rs +++ b/crates/zed_actions2/src/lib.rs @@ -1,5 +1,14 @@ use gpui::{action, actions}; +// If the zed binary doesn't use anything in this crate, it will be optimized away +// and the actions won't initialize. So we just provide an empty initialization function +// to be called from main. +// +// These may provide relevant context: +// https://github.com/rust-lang/rust/issues/47384 +// https://github.com/mmastrac/rust-ctor/issues/280 +pub fn init() {} + actions!( About, DebugElements, From ce30a689a0df57ef0b501ac63c8872c4fff2e55f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 13 Nov 2023 23:15:45 -0700 Subject: [PATCH 073/126] Checkpoint --- crates/gpui2/src/elements/node.rs | 118 +++++++++++++++--------------- 1 file changed, 58 insertions(+), 60 deletions(-) diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index f69ad25ab0..82a952fc16 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -11,12 +11,14 @@ use refineable::Refineable; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, + fmt::Debug, marker::PhantomData, mem, sync::Arc, time::Duration, }; use taffy::style::Overflow; +use util::ResultExt; const DRAG_THRESHOLD: f64 = 2.; const TOOLTIP_DELAY: Duration = Duration::from_millis(500); @@ -30,10 +32,36 @@ pub struct GroupStyle { pub trait InteractiveComponent: Sized + Element { fn interactivity(&mut self) -> &mut Interactivity; - fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + fn id(mut self, id: impl Into) -> Stateful { + self.interactivity().element_id = Some(id.into()); + + Stateful { + element: self, + view_type: PhantomData, + } + } + + fn track_focus(mut self, focus_handle: FocusHandle) -> Focusable { + self.interactivity().focusable = true; + self.interactivity().tracked_focus_handle = Some(focus_handle); + Focusable { + element: self, + view_type: PhantomData, + } + } + + fn key_context(mut self, key_context: C) -> Self where - Self: Sized, + C: TryInto, + E: Debug, { + if let Some(key_context) = key_context.try_into().log_err() { + self.interactivity().key_context = key_context; + } + self + } + + fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { self.interactivity().hover_style = f(StyleRefinement::default()); self } @@ -42,10 +70,7 @@ pub trait InteractiveComponent: Sized + Element { mut self, group_name: impl Into, f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().group_hover_style = Some(GroupStyle { group: group_name.into(), style: f(StyleRefinement::default()), @@ -57,10 +82,7 @@ pub trait InteractiveComponent: Sized + Element { mut self, button: MouseButton, handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().mouse_down_listeners.push(Box::new( move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Bubble @@ -78,10 +100,7 @@ pub trait InteractiveComponent: Sized + Element { mut self, button: MouseButton, handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().mouse_up_listeners.push(Box::new( move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Bubble @@ -98,10 +117,7 @@ pub trait InteractiveComponent: Sized + Element { fn on_mouse_down_out( mut self, handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().mouse_down_listeners.push(Box::new( move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { @@ -116,10 +132,7 @@ pub trait InteractiveComponent: Sized + Element { mut self, button: MouseButton, handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().mouse_up_listeners.push(Box::new( move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Capture @@ -136,10 +149,7 @@ pub trait InteractiveComponent: Sized + Element { fn on_mouse_move( mut self, handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().mouse_move_listeners.push(Box::new( move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { @@ -153,10 +163,7 @@ pub trait InteractiveComponent: Sized + Element { fn on_scroll_wheel( mut self, handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().scroll_wheel_listeners.push(Box::new( move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { @@ -171,10 +178,7 @@ pub trait InteractiveComponent: Sized + Element { fn capture_action( mut self, listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().action_listeners.push(( TypeId::of::(), Box::new(move |view, action, phase, cx| { @@ -191,10 +195,7 @@ pub trait InteractiveComponent: Sized + Element { fn on_action( mut self, listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().action_listeners.push(( TypeId::of::(), Box::new(move |view, action, phase, cx| { @@ -210,10 +211,7 @@ pub trait InteractiveComponent: Sized + Element { fn on_key_down( mut self, listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity() .key_down_listeners .push(Box::new(move |view, event, phase, cx| { @@ -225,10 +223,7 @@ pub trait InteractiveComponent: Sized + Element { fn on_key_up( mut self, listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity() .key_up_listeners .push(Box::new(move |view, event, phase, cx| { @@ -237,10 +232,7 @@ pub trait InteractiveComponent: Sized + Element { self } - fn drag_over(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { + fn drag_over(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { self.interactivity() .drag_over_styles .push((TypeId::of::(), f(StyleRefinement::default()))); @@ -251,10 +243,7 @@ pub trait InteractiveComponent: Sized + Element { mut self, group_name: impl Into, f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().group_drag_over_styles.push(( TypeId::of::(), GroupStyle { @@ -268,10 +257,7 @@ pub trait InteractiveComponent: Sized + Element { fn on_drop( mut self, listener: impl Fn(&mut V, View, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { + ) -> Self { self.interactivity().drop_listeners.push(( TypeId::of::(), Box::new(move |view, dragged_view, cx| { @@ -283,6 +269,11 @@ pub trait InteractiveComponent: Sized + Element { } pub trait StatefulInteractiveComponent>: InteractiveComponent { + fn focusable(mut self) -> Self { + self.interactivity().focusable = true; + self + } + fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -523,6 +514,13 @@ pub type KeyUpListener = pub type ActionListener = Box) + 'static>; +pub fn node() -> Node { + Node { + interactivity: Interactivity::default(), + children: Vec::default(), + } +} + pub struct Node { interactivity: Interactivity, children: Vec>, @@ -1203,8 +1201,8 @@ impl GroupBounds { } pub struct Focusable { - view_type: PhantomData, element: E, + view_type: PhantomData, } impl> FocusableComponent for Focusable {} @@ -1265,8 +1263,8 @@ where } pub struct Stateful { - view_type: PhantomData, element: E, + view_type: PhantomData, } impl StatefulInteractiveComponent for Stateful From ee4957dd477c638f24c197508271aed7ac4f6861 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 00:06:33 -0800 Subject: [PATCH 074/126] Implement most core actions --- Cargo.lock | 3 +- crates/gpui/src/app.rs | 4 + crates/gpui2/src/app.rs | 12 + crates/gpui2/src/window.rs | 8 + crates/install_cli2/Cargo.toml | 1 + crates/install_cli2/src/install_cli2.rs | 5 +- crates/workspace2/src/workspace2.rs | 96 ++-- crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 8 +- crates/zed2/src/open_listener.rs | 4 +- crates/zed2/src/zed2.rs | 715 ++++++++++++++++-------- crates/zed_actions2/src/lib.rs | 27 +- 12 files changed, 570 insertions(+), 315 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97653e124a..a35dfd20cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4198,6 +4198,7 @@ dependencies = [ "anyhow", "gpui2", "log", + "serde", "smol", "util", ] @@ -11402,7 +11403,7 @@ dependencies = [ "ignore", "image", "indexmap 1.9.3", - "install_cli", + "install_cli2", "isahc", "journal2", "language2", diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4b6b9bea73..b732be7455 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2112,6 +2112,10 @@ impl AppContext { AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap()) } + pub fn open_url(&self, url: &str) { + self.platform.open_url(url) + } + pub fn write_to_clipboard(&self, item: ClipboardItem) { self.platform.write_to_clipboard(item); } diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index b6cb3f6307..5463550587 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -431,6 +431,18 @@ impl AppContext { self.platform.activate(ignoring_other_apps); } + pub fn hide(&self) { + self.platform.hide(); + } + + pub fn hide_other_apps(&self) { + self.platform.hide_other_apps(); + } + + pub fn unhide_other_apps(&self) { + self.platform.unhide_other_apps(); + } + /// Returns the list of currently active displays. pub fn displays(&self) -> Vec> { self.platform.displays() diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index eb69b451b3..4ad807b357 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1365,6 +1365,14 @@ impl<'a> WindowContext<'a> { self.window.platform_window.activate(); } + pub fn minimize_window(&self) { + self.window.platform_window.minimize(); + } + + pub fn toggle_full_screen(&self) { + self.window.platform_window.toggle_full_screen(); + } + pub fn prompt( &self, level: PromptLevel, diff --git a/crates/install_cli2/Cargo.toml b/crates/install_cli2/Cargo.toml index 3310e7fbc8..26fe212fe3 100644 --- a/crates/install_cli2/Cargo.toml +++ b/crates/install_cli2/Cargo.toml @@ -14,5 +14,6 @@ test-support = [] smol.workspace = true anyhow.workspace = true log.workspace = true +serde.workspace = true gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } diff --git a/crates/install_cli2/src/install_cli2.rs b/crates/install_cli2/src/install_cli2.rs index 7938d60210..6fd1019c3f 100644 --- a/crates/install_cli2/src/install_cli2.rs +++ b/crates/install_cli2/src/install_cli2.rs @@ -1,10 +1,9 @@ use anyhow::{anyhow, Result}; -use gpui::AsyncAppContext; +use gpui::{actions, AsyncAppContext}; use std::path::Path; use util::ResultExt; -// todo!() -// actions!(cli, [Install]); +actions!(Install); pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??; diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index f5cbc6e787..1a5f3329b8 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -45,7 +45,7 @@ use gpui::{ }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; -use language2::LanguageRegistry; +use language2::{LanguageRegistry, Rope}; use lazy_static::lazy_static; pub use modal_layer::*; use node_runtime::NodeRuntime; @@ -1241,29 +1241,29 @@ impl Workspace { // self.titlebar_item.clone() // } - // /// Call the given callback with a workspace whose project is local. - // /// - // /// If the given workspace has a local project, then it will be passed - // /// to the callback. Otherwise, a new empty window will be created. - // pub fn with_local_workspace( - // &mut self, - // cx: &mut ViewContext, - // callback: F, - // ) -> Task> - // where - // T: 'static, - // F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> T, - // { - // if self.project.read(cx).is_local() { - // Task::Ready(Some(Ok(callback(self, cx)))) - // } else { - // let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx); - // cx.spawn(|_vh, mut cx| async move { - // let (workspace, _) = task.await; - // workspace.update(&mut cx, callback) - // }) - // } - // } + /// Call the given callback with a workspace whose project is local. + /// + /// If the given workspace has a local project, then it will be passed + /// to the callback. Otherwise, a new empty window will be created. + pub fn with_local_workspace( + &mut self, + cx: &mut ViewContext, + callback: F, + ) -> Task> + where + T: 'static, + F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> T, + { + if self.project.read(cx).is_local() { + Task::Ready(Some(Ok(callback(self, cx)))) + } else { + let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx); + cx.spawn(|_vh, mut cx| async move { + let (workspace, _) = task.await?; + workspace.update(&mut cx, callback) + }) + } + } pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator> { self.project.read(cx).worktrees() @@ -4507,32 +4507,32 @@ pub fn open_new( }) } -// pub fn create_and_open_local_file( -// path: &'static Path, -// cx: &mut ViewContext, -// default_content: impl 'static + Send + FnOnce() -> Rope, -// ) -> Task>> { -// cx.spawn(|workspace, mut cx| async move { -// let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; -// if !fs.is_file(path).await { -// fs.create_file(path, Default::default()).await?; -// fs.save(path, &default_content(), Default::default()) -// .await?; -// } +pub fn create_and_open_local_file( + path: &'static Path, + cx: &mut ViewContext, + default_content: impl 'static + Send + FnOnce() -> Rope, +) -> Task>> { + cx.spawn(|workspace, mut cx| async move { + let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; + if !fs.is_file(path).await { + fs.create_file(path, Default::default()).await?; + fs.save(path, &default_content(), Default::default()) + .await?; + } -// let mut items = workspace -// .update(&mut cx, |workspace, cx| { -// workspace.with_local_workspace(cx, |workspace, cx| { -// workspace.open_paths(vec![path.to_path_buf()], false, cx) -// }) -// })? -// .await? -// .await; + let mut items = workspace + .update(&mut cx, |workspace, cx| { + workspace.with_local_workspace(cx, |workspace, cx| { + workspace.open_paths(vec![path.to_path_buf()], false, cx) + }) + })? + .await? + .await; -// let item = items.pop().flatten(); -// item.ok_or_else(|| anyhow!("path {path:?} is not a file"))? -// }) -// } + let item = items.pop().flatten(); + item.ok_or_else(|| anyhow!("path {path:?} is not a file"))? + }) +} // pub fn join_remote_project( // project_id: u64, diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 570912abc5..610bd90f69 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -43,7 +43,7 @@ fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } go_to_line = { package = "go_to_line2", path = "../go_to_line2" } gpui = { package = "gpui2", path = "../gpui2" } -install_cli = { path = "../install_cli" } +install_cli = { package = "install_cli2", path = "../install_cli2" } journal = { package = "journal2", path = "../journal2" } language = { package = "language2", path = "../language2" } # language_selector = { path = "../language_selector" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 9a4ad81806..730dd255f9 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -98,7 +98,7 @@ fn main() { let (listener, mut open_rx) = OpenListener::new(); let listener = Arc::new(listener); let open_listener = listener.clone(); - app.on_open_urls(move |urls, _| open_listener.open_urls(urls)); + app.on_open_urls(move |urls, _| open_listener.open_urls(&urls)); app.on_reopen(move |_cx| { // todo!("workspace") // if cx.has_global::>() { @@ -211,13 +211,13 @@ fn main() { // zed::init(&app_state, cx); // cx.set_menus(menus::menus()); - init_zed_actions(cx); + init_zed_actions(app_state.clone(), cx); if stdout_is_a_pty() { cx.activate(true); let urls = collect_url_args(); if !urls.is_empty() { - listener.open_urls(urls) + listener.open_urls(&urls) } } else { upload_previous_panics(http.clone(), cx); @@ -227,7 +227,7 @@ fn main() { if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() && !listener.triggered.load(Ordering::Acquire) { - listener.open_urls(collect_url_args()) + listener.open_urls(&collect_url_args()) } } diff --git a/crates/zed2/src/open_listener.rs b/crates/zed2/src/open_listener.rs index f4219f199d..4c961a2b31 100644 --- a/crates/zed2/src/open_listener.rs +++ b/crates/zed2/src/open_listener.rs @@ -54,7 +54,7 @@ impl OpenListener { ) } - pub fn open_urls(&self, urls: Vec) { + pub fn open_urls(&self, urls: &[String]) { self.triggered.store(true, Ordering::Release); let request = if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) @@ -101,7 +101,7 @@ impl OpenListener { None } - fn handle_file_urls(&self, urls: Vec) -> Option { + fn handle_file_urls(&self, urls: &[String]) -> Option { let paths: Vec<_> = urls .iter() .flat_map(|url| url.strip_prefix("file://")) diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 54723ee8d8..37c317fb61 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -1,5 +1,5 @@ -#![allow(unused_variables, dead_code, unused_mut)] -// todo!() this is to make transition easier. +#![allow(unused_variables, unused_mut)] +//todo!() mod assets; pub mod languages; @@ -7,18 +7,54 @@ mod only_instance; mod open_listener; pub use assets::*; +use collections::VecDeque; +use editor::{Editor, MultiBuffer}; use gpui::{ - point, px, AppContext, AsyncWindowContext, Task, TitlebarOptions, WeakView, WindowBounds, - WindowKind, WindowOptions, + actions, point, px, AppContext, AsyncWindowContext, Context, PromptLevel, Task, + TitlebarOptions, ViewContext, VisualContext, WeakView, WindowBounds, WindowKind, WindowOptions, }; pub use only_instance::*; pub use open_listener::*; -use anyhow::Result; -use settings::Settings; -use std::sync::Arc; +use anyhow::{anyhow, Context as _, Result}; +use settings::{initial_local_settings_content, Settings}; +use std::{borrow::Cow, ops::Deref, sync::Arc}; +use util::{ + asset_str, + channel::ReleaseChannel, + paths::{self, LOCAL_SETTINGS_RELATIVE_PATH}, + ResultExt, +}; use uuid::Uuid; -use workspace::{AppState, Workspace}; +use workspace::{ + create_and_open_local_file, notifications::simple_message_notification::MessageNotification, + open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, +}; +use zed_actions::{OpenBrowser, OpenZedURL}; + +actions!( + About, + DebugElements, + DecreaseBufferFontSize, + Hide, + HideOthers, + IncreaseBufferFontSize, + Minimize, + OpenDefaultKeymap, + OpenDefaultSettings, + OpenKeymap, + OpenLicenses, + OpenLocalSettings, + OpenLog, + OpenSettings, + OpenTelemetryLog, + Quit, + ResetBufferFontSize, + ResetDatabase, + ShowAll, + ToggleFullScreen, + Zoom, +); pub fn build_window_options( bounds: Option, @@ -48,209 +84,206 @@ pub fn build_window_options( } } -pub fn init_zed_actions(cx: &mut AppContext) { - cx.observe_new_views(|workspace: &mut Workspace, cx| { +pub fn init_zed_actions(app_state: Arc, cx: &mut AppContext) { + cx.observe_new_views(move |workspace: &mut Workspace, _cx| { workspace - // cx.add_action(about); - // cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| { - // cx.platform().hide(); + .register_action(about) + .register_action(|_, _: &Hide, cx| { + cx.hide(); + }) + .register_action(|_, _: &HideOthers, cx| { + cx.hide_other_apps(); + }) + .register_action(|_, _: &ShowAll, cx| { + cx.unhide_other_apps(); + }) + .register_action(|_, _: &Minimize, cx| { + cx.minimize_window(); + }) + .register_action(|_, _: &Zoom, cx| { + cx.zoom_window(); + }) + .register_action(|_, _: &ToggleFullScreen, cx| { + cx.toggle_full_screen(); + }) + .register_action(quit) + .register_action(|_, action: &OpenZedURL, cx| { + cx.global::>() + .open_urls(&[action.url.clone()]) + }) + .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url)) + //todo!(buffer font size) + // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { + // theme::adjust_font_size(cx, |size| *size += 1.0) // }); - // cx.add_global_action(|_: &HideOthers, cx: &mut gpui::AppContext| { - // cx.platform().hide_other_apps(); + // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| { + // theme::adjust_font_size(cx, |size| *size -= 1.0) // }); - // cx.add_global_action(|_: &ShowAll, cx: &mut gpui::AppContext| { - // cx.platform().unhide_other_apps(); + // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx)); + .register_action(|_, _: &install_cli::Install, cx| { + cx.spawn(|_, cx| async move { + install_cli::install_cli(cx.deref()) + .await + .context("error creating CLI symlink") + }) + .detach_and_log_err(cx); + }) + .register_action(|workspace, _: &OpenLog, cx| { + open_log_file(workspace, cx); + }) + .register_action(|workspace, _: &OpenLicenses, cx| { + open_bundled_file( + workspace, + asset_str::("licenses.md"), + "Open Source License Attribution", + "Markdown", + cx, + ); + }) + .register_action( + move |workspace: &mut Workspace, + _: &OpenTelemetryLog, + cx: &mut ViewContext| { + open_telemetry_log_file(workspace, cx); + }, + ) + .register_action( + move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { + create_and_open_local_file(&paths::KEYMAP, cx, Default::default) + .detach_and_log_err(cx); + }, + ) + .register_action( + move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + create_and_open_local_file(&paths::SETTINGS, cx, || { + settings::initial_user_settings_content().as_ref().into() + }) + .detach_and_log_err(cx); + }, + ) + .register_action(open_local_settings_file) + .register_action( + move |workspace: &mut Workspace, + _: &OpenDefaultKeymap, + cx: &mut ViewContext| { + open_bundled_file( + workspace, + settings::default_keymap(), + "Default Key Bindings", + "JSON", + cx, + ); + }, + ) + .register_action( + move |workspace: &mut Workspace, + _: &OpenDefaultSettings, + cx: &mut ViewContext| { + open_bundled_file( + workspace, + settings::default_settings(), + "Default Settings", + "JSON", + cx, + ); + }, + ) + //todo!() + // cx.add_action({ + // move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { + // let app_state = workspace.app_state().clone(); + // let markdown = app_state.languages.language_for_name("JSON"); + // let window = cx.window(); + // cx.spawn(|workspace, mut cx| async move { + // let markdown = markdown.await.log_err(); + // let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| { + // anyhow!("could not debug elements for window {}", window.id()) + // })?) + // .unwrap(); + // workspace + // .update(&mut cx, |workspace, cx| { + // workspace.with_local_workspace(cx, move |workspace, cx| { + // let project = workspace.project().clone(); + // let buffer = project + // .update(cx, |project, cx| { + // project.create_buffer(&content, markdown, cx) + // }) + // .expect("creating buffers on a local workspace always succeeds"); + // let buffer = cx.add_model(|cx| { + // MultiBuffer::singleton(buffer, cx) + // .with_title("Debug Elements".into()) + // }); + // workspace.add_item( + // Box::new(cx.add_view(|cx| { + // Editor::for_multibuffer(buffer, Some(project.clone()), cx) + // })), + // cx, + // ); + // }) + // })? + // .await + // }) + // .detach_and_log_err(cx); + // } // }); - // cx.add_action( - // |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext| { - // cx.minimize_window(); + // .register_action( + // |workspace: &mut Workspace, + // _: &project_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); // }, // ); // cx.add_action( - // |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext| { - // cx.zoom_window(); + // |workspace: &mut Workspace, + // _: &collab_ui::collab_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); // }, // ); // cx.add_action( - // |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext| { - // cx.toggle_full_screen(); + // |workspace: &mut Workspace, + // _: &collab_ui::chat_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); // }, // ); - .register_action(|workspace, _: &zed_actions::Quit, cx| quit(cx)); - // cx.add_global_action(move |action: &OpenZedURL, cx| { - // cx.global::>() - // .open_urls(vec![action.url.clone()]) - // }); - // cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); - // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { - // theme::adjust_font_size(cx, |size| *size += 1.0) - // }); - // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| { - // theme::adjust_font_size(cx, |size| *size -= 1.0) - // }); - // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx)); - // cx.add_global_action(move |_: &install_cli::Install, cx| { - // cx.spawn(|cx| async move { - // install_cli::install_cli(&cx) - // .await - // .context("error creating CLI symlink") - // }) - // .detach_and_log_err(cx); - // }); - // cx.add_action( - // move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext| { - // open_log_file(workspace, cx); - // }, - // ); - // cx.add_action( - // move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext| { - // open_bundled_file( - // workspace, - // asset_str::("licenses.md"), - // "Open Source License Attribution", - // "Markdown", - // cx, - // ); - // }, - // ); - // cx.add_action( - // move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext| { - // open_telemetry_log_file(workspace, cx); - // }, - // ); - // cx.add_action( - // move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { - // create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx); - // }, - // ); - // cx.add_action( - // move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { - // create_and_open_local_file(&paths::SETTINGS, cx, || { - // settings::initial_user_settings_content().as_ref().into() - // }) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action(open_local_settings_file); - // cx.add_action( - // move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { - // open_bundled_file( - // workspace, - // settings::default_keymap(), - // "Default Key Bindings", - // "JSON", - // cx, - // ); - // }, - // ); - // cx.add_action( - // move |workspace: &mut Workspace, - // _: &OpenDefaultSettings, - // cx: &mut ViewContext| { - // open_bundled_file( - // workspace, - // settings::default_settings(), - // "Default Settings", - // "JSON", - // cx, - // ); - // }, - // ); - // cx.add_action({ - // move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { - // let app_state = workspace.app_state().clone(); - // let markdown = app_state.languages.language_for_name("JSON"); - // let window = cx.window(); - // cx.spawn(|workspace, mut cx| async move { - // let markdown = markdown.await.log_err(); - // let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| { - // anyhow!("could not debug elements for window {}", window.id()) - // })?) - // .unwrap(); - // workspace - // .update(&mut cx, |workspace, cx| { - // workspace.with_local_workspace(cx, move |workspace, cx| { - // let project = workspace.project().clone(); - - // let buffer = project - // .update(cx, |project, cx| { - // project.create_buffer(&content, markdown, cx) - // }) - // .expect("creating buffers on a local workspace always succeeds"); - // let buffer = cx.add_model(|cx| { - // MultiBuffer::singleton(buffer, cx) - // .with_title("Debug Elements".into()) - // }); - // workspace.add_item( - // Box::new(cx.add_view(|cx| { - // Editor::for_multibuffer(buffer, Some(project.clone()), cx) - // })), - // cx, - // ); - // }) - // })? - // .await - // }) - // .detach_and_log_err(cx); - // } - // }); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &project_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &collab_ui::collab_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &collab_ui::chat_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &collab_ui::notification_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &terminal_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_global_action({ - // let app_state = Arc::downgrade(&app_state); - // move |_: &NewWindow, cx: &mut AppContext| { - // if let Some(app_state) = app_state.upgrade() { - // open_new(&app_state, cx, |workspace, cx| { - // Editor::new_file(workspace, &Default::default(), cx) - // }) - // .detach(); - // } - // } - // }); - // cx.add_global_action({ - // let app_state = Arc::downgrade(&app_state); - // move |_: &NewFile, cx: &mut AppContext| { - // if let Some(app_state) = app_state.upgrade() { - // open_new(&app_state, cx, |workspace, cx| { - // Editor::new_file(workspace, &Default::default(), cx) - // }) - // .detach(); - // } - // } - // }); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::notification_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &terminal_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + .register_action({ + let app_state = Arc::downgrade(&app_state); + move |_, _: &NewWindow, cx| { + if let Some(app_state) = app_state.upgrade() { + open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + } + }) + .register_action({ + let app_state = Arc::downgrade(&app_state); + move |_, _: &NewFile, cx| { + if let Some(app_state) = app_state.upgrade() { + open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + } + }); + //todo!() // load_default_keymap(cx); }) .detach(); @@ -415,46 +448,58 @@ pub fn initialize_workspace( }) } -fn quit(cx: &mut gpui::AppContext) { - let should_confirm = workspace::WorkspaceSettings::get_global(cx).confirm_quit; - cx.spawn(|mut cx| async move { - // let mut workspace_windows = cx - // .windows() - // .into_iter() - // .filter_map(|window| window.downcast::()) - // .collect::>(); +fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { + let app_name = cx.global::().display_name(); + let version = env!("CARGO_PKG_VERSION"); + let prompt = cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]); + cx.foreground_executor() + .spawn(async { + prompt.await.ok(); + }) + .detach(); +} - // // If multiple windows have unsaved changes, and need a save prompt, - // // prompt in the active window before switching to a different window. - // workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); +fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext) { + let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; + cx.spawn(|_, mut cx| async move { + let mut workspace_windows = cx.update(|_, cx| { + cx.windows() + .into_iter() + .filter_map(|window| window.downcast::()) + .collect::>() + })?; - // if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { - // let answer = window.prompt( - // PromptLevel::Info, - // "Are you sure you want to quit?", - // &["Quit", "Cancel"], - // &mut cx, - // ); + // // If multiple windows have unsaved changes, and need a save prompt, + // // prompt in the active window before switching to a different window. + // workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); - // if let Some(mut answer) = answer { - // let answer = answer.next().await; - // if answer != Some(0) { - // return Ok(()); - // } + // if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { + // let answer = window.prompt( + // PromptLevel::Info, + // "Are you sure you want to quit?", + // &["Quit", "Cancel"], + // &mut cx, + // ); + + // if let Some(mut answer) = answer { + // let answer = answer.next().await; + // if answer != Some(0) { + // return Ok(()); // } // } + // } - // // If the user cancels any save prompt, then keep the app open. - // for window in workspace_windows { - // if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { - // workspace.prepare_to_close(true, cx) - // }) { - // if !should_close.await? { - // return Ok(()); - // } + // // If the user cancels any save prompt, then keep the app open. + // for window in workspace_windows { + // if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { + // workspace.prepare_to_close(true, cx) + // }) { + // if !should_close.await? { + // return Ok(()); // } // } - cx.update(|cx| { + // } + cx.update(|_, cx| { cx.quit(); })?; @@ -462,3 +507,211 @@ fn quit(cx: &mut gpui::AppContext) { }) .detach_and_log_err(cx); } + +fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { + const MAX_LINES: usize = 1000; + workspace + .with_local_workspace(cx, move |workspace, cx| { + let fs = workspace.app_state().fs.clone(); + cx.spawn(|workspace, mut cx| async move { + let (old_log, new_log) = + futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG)); + + let mut lines = VecDeque::with_capacity(MAX_LINES); + for line in old_log + .iter() + .flat_map(|log| log.lines()) + .chain(new_log.iter().flat_map(|log| log.lines())) + { + if lines.len() == MAX_LINES { + lines.pop_front(); + } + lines.push_back(line); + } + let log = lines + .into_iter() + .flat_map(|line| [line, "\n"]) + .collect::(); + + workspace + .update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .expect("creating buffers on a local workspace always succeeds"); + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx)); + + let buffer = cx.build_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title("Log".into()) + }); + workspace.add_item( + Box::new(cx.build_view(|cx| { + Editor::for_multibuffer(buffer, Some(project), cx) + })), + cx, + ); + }) + .log_err(); + }) + .detach(); + }) + .detach(); +} + +fn open_local_settings_file( + workspace: &mut Workspace, + _: &OpenLocalSettings, + cx: &mut ViewContext, +) { + let project = workspace.project().clone(); + let worktree = project + .read(cx) + .visible_worktrees(cx) + .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); + if let Some(worktree) = worktree { + let tree_id = worktree.read(cx).id(); + cx.spawn(|workspace, mut cx| async move { + let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH; + + if let Some(dir_path) = file_path.parent() { + if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? { + project + .update(&mut cx, |project, cx| { + project.create_entry((tree_id, dir_path), true, cx) + })? + .ok_or_else(|| anyhow!("worktree was removed"))? + .await?; + } + } + + if worktree.update(&mut cx, |tree, _| tree.entry_for_path(file_path).is_none())? { + project + .update(&mut cx, |project, cx| { + project.create_entry((tree_id, file_path), false, cx) + })? + .ok_or_else(|| anyhow!("worktree was removed"))? + .await?; + } + + let editor = workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path((tree_id, file_path), None, true, cx) + })? + .await? + .downcast::() + .ok_or_else(|| anyhow!("unexpected item type"))?; + + editor + .downgrade() + .update(&mut cx, |editor, cx| { + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + if buffer.read(cx).is_empty() { + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, initial_local_settings_content())], None, cx) + }); + } + } + }) + .ok(); + + anyhow::Ok(()) + }) + .detach(); + } else { + workspace.show_notification(0, cx, |cx| { + cx.build_view(|_| MessageNotification::new("This project has no folders open.")) + }) + } +} + +fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { + workspace.with_local_workspace(cx, move |workspace, cx| { + let app_state = workspace.app_state().clone(); + cx.spawn(|workspace, mut cx| async move { + async fn fetch_log_string(app_state: &Arc) -> Option { + let path = app_state.client.telemetry().log_file_path()?; + app_state.fs.load(&path).await.log_err() + } + + let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string()); + + const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024; + let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN); + if let Some(newline_offset) = log[start_offset..].find('\n') { + start_offset += newline_offset + 1; + } + let log_suffix = &log[start_offset..]; + let json = app_state.languages.language_for_name("JSON").await.log_err(); + + workspace.update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .expect("creating buffers on a local workspace always succeeds"); + buffer.update(cx, |buffer, cx| { + buffer.set_language(json, cx); + buffer.edit( + [( + 0..0, + concat!( + "// Zed collects anonymous usage data to help us understand how people are using the app.\n", + "// Telemetry can be disabled via the `settings.json` file.\n", + "// Here is the data that has been reported for the current session:\n", + "\n" + ), + )], + None, + cx, + ); + buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx); + }); + + let buffer = cx.build_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into()) + }); + workspace.add_item( + Box::new(cx.build_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))), + cx, + ); + }).log_err()?; + + Some(()) + }) + .detach(); + }).detach(); +} + +fn open_bundled_file( + workspace: &mut Workspace, + text: Cow<'static, str>, + title: &'static str, + language: &'static str, + cx: &mut ViewContext, +) { + let language = workspace.app_state().languages.language_for_name(language); + cx.spawn(|workspace, mut cx| async move { + let language = language.await.log_err(); + workspace + .update(&mut cx, |workspace, cx| { + workspace.with_local_workspace(cx, |workspace, cx| { + let project = workspace.project(); + let buffer = project.update(cx, move |project, cx| { + project + .create_buffer(text.as_ref(), language, cx) + .expect("creating buffers on a local workspace always succeeds") + }); + let buffer = cx.build_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title(title.into()) + }); + workspace.add_item( + Box::new(cx.build_view(|cx| { + Editor::for_multibuffer(buffer, Some(project.clone()), cx) + })), + cx, + ); + }) + })? + .await + }) + .detach_and_log_err(cx); +} diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs index 097766492f..7f0c19853e 100644 --- a/crates/zed_actions2/src/lib.rs +++ b/crates/zed_actions2/src/lib.rs @@ -1,4 +1,4 @@ -use gpui::{action, actions}; +use gpui::action; // If the zed binary doesn't use anything in this crate, it will be optimized away // and the actions won't initialize. So we just provide an empty initialization function @@ -9,34 +9,11 @@ use gpui::{action, actions}; // https://github.com/mmastrac/rust-ctor/issues/280 pub fn init() {} -actions!( - About, - DebugElements, - DecreaseBufferFontSize, - Hide, - HideOthers, - IncreaseBufferFontSize, - Minimize, - OpenDefaultKeymap, - OpenDefaultSettings, - OpenKeymap, - OpenLicenses, - OpenLocalSettings, - OpenLog, - OpenSettings, - OpenTelemetryLog, - Quit, - ResetBufferFontSize, - ResetDatabase, - ShowAll, - ToggleFullScreen, - Zoom, -); - #[action] pub struct OpenBrowser { pub url: String, } + #[action] pub struct OpenZedURL { pub url: String, From 8aa076692ea2ad01eb9bf9a461b913e4882426fd Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 00:09:42 -0800 Subject: [PATCH 075/126] Add missing global --- crates/zed2/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 730dd255f9..b3797bed2a 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -113,6 +113,8 @@ fn main() { app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); + cx.set_global(listener.clone()); + load_embedded_fonts(cx); let mut store = SettingsStore::default(); From 27fb381ccab269685dbae629c3db91ab1138a7f3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 01:15:48 -0700 Subject: [PATCH 076/126] Checkpoint --- .../command_palette2/src/command_palette.rs | 10 +- crates/editor2/src/editor.rs | 10 +- crates/editor2/src/element.rs | 2 +- crates/editor2/src/items.rs | 2 +- crates/go_to_line2/src/go_to_line.rs | 8 +- crates/gpui2/src/element.rs | 12 +- crates/gpui2/src/elements.rs | 5 +- crates/gpui2/src/elements/div.rs | 42 - crates/gpui2/src/elements/img.rs | 168 +--- crates/gpui2/src/elements/node.rs | 217 +++-- crates/gpui2/src/elements/svg.rs | 125 +-- crates/gpui2/src/elements/text.rs | 2 +- crates/gpui2/src/elements/uniform_list.rs | 223 +++-- crates/gpui2/src/interactive.rs | 920 +----------------- crates/gpui2/src/key_dispatch.rs | 118 +-- crates/gpui2/src/prelude.rs | 5 +- crates/gpui2/src/styled.rs | 288 ++---- crates/gpui2/src/view.rs | 2 +- crates/gpui2_macros/src/style_helpers.rs | 4 +- crates/picker2/src/picker2.rs | 6 +- crates/storybook2/src/stories/colors.rs | 4 +- crates/storybook2/src/stories/focus.rs | 11 +- crates/storybook2/src/stories/kitchen_sink.rs | 4 +- crates/storybook2/src/stories/picker.rs | 12 +- crates/storybook2/src/stories/scroll.rs | 5 +- crates/storybook2/src/stories/text.rs | 4 +- crates/storybook2/src/stories/z_index.rs | 6 +- crates/storybook2/src/storybook2.rs | 4 +- crates/theme2/src/players.rs | 4 +- crates/theme2/src/story.rs | 4 +- crates/ui2/src/components/avatar.rs | 4 +- crates/ui2/src/components/button.rs | 6 +- crates/ui2/src/components/checkbox.rs | 10 +- crates/ui2/src/components/context_menu.rs | 4 +- crates/ui2/src/components/details.rs | 4 +- crates/ui2/src/components/elevated_surface.rs | 6 +- crates/ui2/src/components/facepile.rs | 4 +- crates/ui2/src/components/icon.rs | 4 +- crates/ui2/src/components/icon_button.rs | 6 +- crates/ui2/src/components/input.rs | 9 +- crates/ui2/src/components/keybinding.rs | 4 +- crates/ui2/src/components/label.rs | 4 +- crates/ui2/src/components/modal.rs | 2 +- crates/ui2/src/components/palette.rs | 8 +- crates/ui2/src/components/panel.rs | 8 +- crates/ui2/src/components/stack.rs | 6 +- crates/ui2/src/components/tab.rs | 6 +- crates/ui2/src/components/toast.rs | 11 +- crates/ui2/src/components/toggle.rs | 2 +- crates/ui2/src/components/tooltip.rs | 4 +- crates/ui2/src/prelude.rs | 4 +- crates/ui2/src/story.rs | 4 +- crates/ui2/src/styled_ext.rs | 11 +- crates/ui2/src/to_extract/assistant_panel.rs | 6 +- crates/ui2/src/to_extract/breadcrumb.rs | 10 +- crates/ui2/src/to_extract/buffer.rs | 4 +- crates/ui2/src/to_extract/buffer_search.rs | 6 +- crates/ui2/src/to_extract/chat_panel.rs | 9 +- crates/ui2/src/to_extract/collab_panel.rs | 9 +- crates/ui2/src/to_extract/command_palette.rs | 4 +- crates/ui2/src/to_extract/copilot.rs | 4 +- crates/ui2/src/to_extract/editor_pane.rs | 6 +- .../ui2/src/to_extract/language_selector.rs | 4 +- crates/ui2/src/to_extract/multi_buffer.rs | 4 +- .../ui2/src/to_extract/notifications_panel.rs | 13 +- crates/ui2/src/to_extract/panes.rs | 2 +- crates/ui2/src/to_extract/project_panel.rs | 9 +- crates/ui2/src/to_extract/recent_projects.rs | 4 +- crates/ui2/src/to_extract/tab_bar.rs | 8 +- crates/ui2/src/to_extract/terminal.rs | 4 +- crates/ui2/src/to_extract/theme_selector.rs | 4 +- crates/ui2/src/to_extract/title_bar.rs | 10 +- crates/ui2/src/to_extract/toolbar.rs | 4 +- crates/ui2/src/to_extract/traffic_lights.rs | 4 +- crates/ui2/src/to_extract/workspace.rs | 8 +- crates/workspace2/src/dock.rs | 12 +- crates/workspace2/src/modal_layer.rs | 6 +- crates/workspace2/src/notifications.rs | 4 +- crates/workspace2/src/pane.rs | 12 +- crates/workspace2/src/status_bar.rs | 6 +- crates/workspace2/src/toolbar.rs | 4 +- crates/workspace2/src/workspace2.rs | 30 +- 82 files changed, 661 insertions(+), 1907 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index c7a6c9ee83..139d8a16c7 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,9 +1,9 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke, - ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext, - WeakView, WindowContext, + actions, div, prelude::*, Action, AppContext, Component, EventEmitter, FocusHandle, Keystroke, + Node, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, + WindowContext, }; use picker::{Picker, PickerDelegate}; use std::{ @@ -77,7 +77,7 @@ impl Modal for CommandPalette { } impl Render for CommandPalette { - type Element = Div; + type Element = Node; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { v_stack().w_96().child(self.picker.clone()) @@ -148,7 +148,7 @@ impl CommandPaletteDelegate { } impl PickerDelegate for CommandPaletteDelegate { - type ListItem = Div>; + type ListItem = Node>; fn placeholder_text(&self) -> Arc { "Execute a command...".into() diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 654aa73fee..f4e4146f1a 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,12 +39,12 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext, - AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, + action, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, AnyElement, + AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, - InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, - StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, - ViewContext, VisualContext, WeakView, WindowContext, + InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled, + Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, + WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 8ee99e19af..ba2dcb431a 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2442,7 +2442,7 @@ enum Invisible { impl Element for EditorElement { type ElementState = (); - fn id(&self) -> Option { + fn element_id(&self) -> Option { None } diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 25e9f91608..b1fc69ecac 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -9,7 +9,7 @@ use collections::HashSet; use futures::future::try_join_all; use gpui::{ div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter, - FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, + FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::{ diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 50592901b5..9f2ba6db21 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,7 +1,7 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString, - StatelessInteractive, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + actions, div, prelude::*, AppContext, EventEmitter, Node, ParentComponent, Render, + SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; @@ -145,11 +145,11 @@ impl GoToLine { } impl Render for GoToLine { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { modal(cx) - .context("GoToLine") + .key_context("GoToLine") .on_action(Self::cancel) .on_action(Self::confirm) .w_96() diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 80ab0abc90..46ea5c6cd2 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -8,7 +8,7 @@ use std::{any::Any, mem}; pub trait Element { type ElementState: 'static; - fn id(&self) -> Option; + fn element_id(&self) -> Option; /// Called to initialize this element for the current frame. If this /// element had state in a previous frame, it will be passed in for the 3rd argument. @@ -38,7 +38,7 @@ pub trait Element { #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] pub struct GlobalElementId(SmallVec<[ElementId; 32]>); -pub trait ParentElement { +pub trait ParentComponent { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; fn child(mut self, child: impl Component) -> Self @@ -120,7 +120,7 @@ where E::ElementState: 'static, { fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext) { - let frame_state = if let Some(id) = self.element.id() { + let frame_state = if let Some(id) = self.element.element_id() { cx.with_element_state(id, |element_state, cx| { let element_state = self.element.initialize(view_state, element_state, cx); ((), element_state) @@ -142,7 +142,7 @@ where frame_state: initial_frame_state, } => { frame_state = initial_frame_state; - if let Some(id) = self.element.id() { + if let Some(id) = self.element.element_id() { layout_id = cx.with_element_state(id, |element_state, cx| { let mut element_state = element_state.unwrap(); let layout_id = self.element.layout(state, &mut element_state, cx); @@ -181,7 +181,7 @@ where .. } => { let bounds = cx.layout_bounds(layout_id); - if let Some(id) = self.element.id() { + if let Some(id) = self.element.element_id() { cx.with_element_state(id, |element_state, cx| { let mut element_state = element_state.unwrap(); self.element @@ -351,7 +351,7 @@ where { type ElementState = AnyElement; - fn id(&self) -> Option { + fn element_id(&self) -> Option { None } diff --git a/crates/gpui2/src/elements.rs b/crates/gpui2/src/elements.rs index e0e155fb03..5bf9e6202b 100644 --- a/crates/gpui2/src/elements.rs +++ b/crates/gpui2/src/elements.rs @@ -1,12 +1,13 @@ -mod div; +// mod div; mod img; mod node; mod svg; mod text; mod uniform_list; -pub use div::*; +// pub use div::*; pub use img::*; +pub use node::*; pub use svg::*; pub use text::*; pub use uniform_list::*; diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 1d85450c29..537c146339 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -55,16 +55,6 @@ where I: ElementInteractivity, F: KeyDispatch, { - pub fn group(mut self, group: impl Into) -> Self { - self.group = Some(group.into()); - self - } - - pub fn z_index(mut self, z_index: u32) -> Self { - self.base_style.z_index = Some(z_index); - self - } - pub fn context(mut self, context: C) -> Self where Self: Sized, @@ -77,22 +67,6 @@ where self } - pub fn overflow_hidden(mut self) -> Self { - self.base_style.overflow.x = Some(Overflow::Hidden); - self.base_style.overflow.y = Some(Overflow::Hidden); - self - } - - pub fn overflow_hidden_x(mut self) -> Self { - self.base_style.overflow.x = Some(Overflow::Hidden); - self - } - - pub fn overflow_hidden_y(mut self) -> Self { - self.base_style.overflow.y = Some(Overflow::Hidden); - self - } - pub fn compute_style( &self, bounds: Bounds, @@ -135,22 +109,6 @@ impl Div, NonFocusableKeyDispatch> { base_style: self.base_style, } } - - pub fn overflow_scroll(mut self) -> Self { - self.base_style.overflow.x = Some(Overflow::Scroll); - self.base_style.overflow.y = Some(Overflow::Scroll); - self - } - - pub fn overflow_x_scroll(mut self) -> Self { - self.base_style.overflow.x = Some(Overflow::Scroll); - self - } - - pub fn overflow_y_scroll(mut self) -> Self { - self.base_style.overflow.y = Some(Overflow::Scroll); - self - } } impl Div, NonFocusableKeyDispatch> { diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 1ff088c1af..bfa3c6cfae 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,35 +1,28 @@ use crate::{ - div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementId, - ElementInteractivity, FocusListeners, Focusable, FocusableKeyDispatch, KeyDispatch, LayoutId, - NonFocusableKeyDispatch, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, - StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, + AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent, + InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, + Styled, ViewContext, }; use futures::FutureExt; use util::ResultExt; -pub struct Img< - V: 'static, - I: ElementInteractivity = StatelessInteractivity, - F: KeyDispatch = NonFocusableKeyDispatch, -> { - base: Div, +pub struct Img { + interactivity: Interactivity, uri: Option, grayscale: bool, } -pub fn img() -> Img, NonFocusableKeyDispatch> { +pub fn img() -> Img { Img { - base: div(), + interactivity: Interactivity::default(), uri: None, grayscale: false, } } -impl Img +impl Img where V: 'static, - I: ElementInteractivity, - F: KeyDispatch, { pub fn uri(mut self, uri: impl Into) -> Self { self.uri = Some(uri.into()); @@ -42,38 +35,17 @@ where } } -impl Img, F> -where - F: KeyDispatch, -{ - pub fn id(self, id: impl Into) -> Img, F> { - Img { - base: self.base.id(id), - uri: self.uri, - grayscale: self.grayscale, - } - } -} - -impl Component for Img -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Component for Img { fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Img -where - I: ElementInteractivity, - F: KeyDispatch, -{ - type ElementState = DivState; +impl Element for Img { + type ElementState = InteractiveElementState; - fn id(&self) -> Option { - self.base.id() + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() } fn initialize( @@ -82,7 +54,7 @@ where element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - self.base.initialize(view_state, element_state, cx) + self.interactivity.initialize(element_state, cx) } fn layout( @@ -91,7 +63,9 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - self.base.layout(view_state, element_state, cx) + self.interactivity.layout(element_state, cx, |style, cx| { + cx.request_layout(&style, None) + }) } fn paint( @@ -101,86 +75,50 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - cx.with_z_index(0, |cx| { - self.base.paint(bounds, view, element_state, cx); - }); + self.interactivity.paint( + bounds, + bounds.size, + element_state, + cx, + |style, scroll_offset, cx| { + let corner_radii = style.corner_radii; - let style = self.base.compute_style(bounds, element_state, cx); - let corner_radii = style.corner_radii; - - if let Some(uri) = self.uri.clone() { - // eprintln!(">>> image_cache.get({uri}"); - let image_future = cx.image_cache.get(uri.clone()); - // eprintln!("<<< image_cache.get({uri}"); - if let Some(data) = image_future - .clone() - .now_or_never() - .and_then(ResultExt::log_err) - { - let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); - cx.with_z_index(1, |cx| { - cx.paint_image(bounds, corner_radii, data, self.grayscale) - .log_err() - }); - } else { - cx.spawn(|_, mut cx| async move { - if image_future.await.log_err().is_some() { - cx.on_next_frame(|cx| cx.notify()); + if let Some(uri) = self.uri.clone() { + // eprintln!(">>> image_cache.get({uri}"); + let image_future = cx.image_cache.get(uri.clone()); + // eprintln!("<<< image_cache.get({uri}"); + if let Some(data) = image_future + .clone() + .now_or_never() + .and_then(ResultExt::log_err) + { + let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); + cx.with_z_index(1, |cx| { + cx.paint_image(bounds, corner_radii, data, self.grayscale) + .log_err() + }); + } else { + cx.spawn(|_, mut cx| async move { + if image_future.await.log_err().is_some() { + cx.on_next_frame(|cx| cx.notify()); + } + }) + .detach() } - }) - .detach() - } - } + } + }, + ) } } -impl Styled for Img -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Styled for Img { fn style(&mut self) -> &mut StyleRefinement { - self.base.style() + &mut self.interactivity.base_style } } -impl StatelessInteractive for Img -where - I: ElementInteractivity, - F: KeyDispatch, -{ - fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { - self.base.stateless_interactivity() - } -} - -impl StatefulInteractive for Img, F> -where - F: KeyDispatch, -{ - fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { - self.base.stateful_interactivity() - } -} - -impl Focusable for Img> -where - V: 'static, - I: ElementInteractivity, -{ - fn focus_listeners(&mut self) -> &mut FocusListeners { - self.base.focus_listeners() - } - - fn set_focus_style(&mut self, style: StyleRefinement) { - self.base.set_focus_style(style) - } - - fn set_focus_in_style(&mut self, style: StyleRefinement) { - self.base.set_focus_in_style(style) - } - - fn set_in_focus_style(&mut self, style: StyleRefinement) { - self.base.set_in_focus_style(style) +impl InteractiveComponent for Img { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity } } diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index 82a952fc16..49c447254e 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -1,9 +1,9 @@ use crate::{ point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, - BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, - KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, - StyleRefinement, Styled, Task, View, ViewContext, Visibility, + BorrowWindow, Bounds, ClickEvent, Component, DispatchPhase, Element, ElementId, FocusEvent, + FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, Point, Render, ScrollWheelEvent, + SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility, }; use collections::HashMap; use parking_lot::Mutex; @@ -32,6 +32,11 @@ pub struct GroupStyle { pub trait InteractiveComponent: Sized + Element { fn interactivity(&mut self) -> &mut Interactivity; + fn group(mut self, group: impl Into) -> Self { + self.interactivity().group = Some(group.into()); + self + } + fn id(mut self, id: impl Into) -> Stateful { self.interactivity().element_id = Some(id.into()); @@ -41,9 +46,9 @@ pub trait InteractiveComponent: Sized + Element { } } - fn track_focus(mut self, focus_handle: FocusHandle) -> Focusable { + fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable { self.interactivity().focusable = true; - self.interactivity().tracked_focus_handle = Some(focus_handle); + self.interactivity().tracked_focus_handle = Some(focus_handle.clone()); Focusable { element: self, view_type: PhantomData, @@ -269,8 +274,27 @@ pub trait InteractiveComponent: Sized + Element { } pub trait StatefulInteractiveComponent>: InteractiveComponent { - fn focusable(mut self) -> Self { + fn focusable(mut self) -> Focusable { self.interactivity().focusable = true; + Focusable { + element: self, + view_type: PhantomData, + } + } + + fn overflow_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); + self + } + + fn overflow_x_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); + self + } + + fn overflow_y_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); self } @@ -514,16 +538,16 @@ pub type KeyUpListener = pub type ActionListener = Box) + 'static>; -pub fn node() -> Node { +pub fn div() -> Node { Node { interactivity: Interactivity::default(), - children: Vec::default(), + children: SmallVec::default(), } } pub struct Node { interactivity: Interactivity, - children: Vec>, + children: SmallVec<[AnyElement; 2]>, } impl Styled for Node { @@ -538,10 +562,16 @@ impl InteractiveComponent for Node { } } +impl ParentComponent for Node { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + impl Element for Node { type ElementState = NodeState; - fn id(&self) -> Option { + fn element_id(&self) -> Option { self.interactivity.element_id.clone() } @@ -641,48 +671,54 @@ impl Element for Node { } } +impl Component for Node { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + pub struct NodeState { child_layout_ids: SmallVec<[LayoutId; 4]>, interactive_state: InteractiveElementState, } pub struct Interactivity { - element_id: Option, - key_context: KeyContext, - focusable: bool, - tracked_focus_handle: Option, - focus_listeners: FocusListeners, - scroll_offset: Point, - group: Option, - base_style: StyleRefinement, - focus_style: StyleRefinement, - focus_in_style: StyleRefinement, - in_focus_style: StyleRefinement, - hover_style: StyleRefinement, - group_hover_style: Option, - active_style: StyleRefinement, - group_active_style: Option, - drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, - group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, - mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, - mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, - mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, - scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, - key_down_listeners: SmallVec<[KeyDownListener; 2]>, - key_up_listeners: SmallVec<[KeyUpListener; 2]>, - action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, - drop_listeners: SmallVec<[(TypeId, Box>); 2]>, - click_listeners: SmallVec<[ClickListener; 2]>, - drag_listener: Option>, - hover_listener: Option>, - tooltip_builder: Option>, + pub element_id: Option, + pub key_context: KeyContext, + pub focusable: bool, + pub tracked_focus_handle: Option, + pub focus_listeners: FocusListeners, + // pub scroll_offset: Point, + pub group: Option, + pub base_style: StyleRefinement, + pub focus_style: StyleRefinement, + pub focus_in_style: StyleRefinement, + pub in_focus_style: StyleRefinement, + pub hover_style: StyleRefinement, + pub group_hover_style: Option, + pub active_style: StyleRefinement, + pub group_active_style: Option, + pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, + pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, + pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, + pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, + pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, + pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, + pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, + pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, + pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, + pub drop_listeners: SmallVec<[(TypeId, Box>); 2]>, + pub click_listeners: SmallVec<[ClickListener; 2]>, + pub drag_listener: Option>, + pub hover_listener: Option>, + pub tooltip_builder: Option>, } impl Interactivity where V: 'static, { - fn initialize( + pub fn initialize( &mut self, element_state: Option, cx: &mut ViewContext, @@ -703,7 +739,7 @@ where element_state } - fn layout( + pub fn layout( &mut self, element_state: &mut InteractiveElementState, cx: &mut ViewContext, @@ -719,7 +755,7 @@ where }) } - fn paint( + pub fn paint( &mut self, bounds: Bounds, content_size: Size, @@ -996,6 +1032,11 @@ where GroupBounds::push(group, bounds, cx); } + let scroll_offset = element_state + .scroll_offset + .as_ref() + .map(|scroll_offset| *scroll_offset.lock()); + cx.with_element_id(self.element_id.clone(), |cx| { cx.with_key_dispatch( self.key_context.clone(), @@ -1026,7 +1067,7 @@ where } } - f(style, self.scroll_offset, cx) + f(style, scroll_offset.unwrap_or_default(), cx) }, ); }); @@ -1036,7 +1077,7 @@ where } } - fn compute_style( + pub fn compute_style( &self, bounds: Option>, element_state: &mut InteractiveElementState, @@ -1118,7 +1159,7 @@ impl Default for Interactivity { focusable: false, tracked_focus_handle: None, focus_listeners: SmallVec::default(), - scroll_offset: Point::default(), + // scroll_offset: Point::default(), group: None, base_style: StyleRefinement::default(), focus_style: StyleRefinement::default(), @@ -1148,15 +1189,15 @@ impl Default for Interactivity { #[derive(Default)] pub struct InteractiveElementState { - focus_handle: Option, - clicked_state: Arc>, - hover_state: Arc>, - pending_mouse_down: Arc>>, - scroll_offset: Option>>>, - active_tooltip: Arc>>, + pub focus_handle: Option, + pub clicked_state: Arc>, + pub hover_state: Arc>, + pub pending_mouse_down: Arc>>, + pub scroll_offset: Option>>>, + pub active_tooltip: Arc>>, } -struct ActiveTooltip { +pub struct ActiveTooltip { #[allow(unused)] // used to drop the task waiting: Option>, tooltip: Option, @@ -1164,7 +1205,7 @@ struct ActiveTooltip { /// Whether or not the element or a group that contains it is clicked by the mouse. #[derive(Copy, Clone, Default, Eq, PartialEq)] -struct ElementClickedState { +pub struct ElementClickedState { pub group: bool, pub element: bool, } @@ -1222,6 +1263,16 @@ impl> StatefulInteractiveCompo { } +impl Styled for Focusable +where + V: 'static, + E: Styled, +{ + fn style(&mut self) -> &mut StyleRefinement { + self.element.style() + } +} + impl Element for Focusable where V: 'static, @@ -1229,8 +1280,8 @@ where { type ElementState = E::ElementState; - fn id(&self) -> Option { - self.element.id() + fn element_id(&self) -> Option { + self.element.element_id() } fn initialize( @@ -1262,11 +1313,41 @@ where } } +impl Component for Focusable +where + V: 'static, + E: 'static + Element, +{ + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + +impl ParentComponent for Focusable +where + V: 'static, + E: ParentComponent, +{ + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + self.element.children_mut() + } +} + pub struct Stateful { element: E, view_type: PhantomData, } +impl Styled for Stateful +where + V: 'static, + E: Styled, +{ + fn style(&mut self) -> &mut StyleRefinement { + self.element.style() + } +} + impl StatefulInteractiveComponent for Stateful where V: 'static, @@ -1294,8 +1375,8 @@ where { type ElementState = E::ElementState; - fn id(&self) -> Option { - self.element.id() + fn element_id(&self) -> Option { + self.element.element_id() } fn initialize( @@ -1326,3 +1407,23 @@ where self.element.paint(bounds, view_state, element_state, cx) } } + +impl Component for Stateful +where + V: 'static, + E: 'static + Element, +{ + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + +impl ParentComponent for Stateful +where + V: 'static, + E: ParentComponent, +{ + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + self.element.children_mut() + } +} diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index bafedb7f2d..d0b321c0e2 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,69 +1,40 @@ use crate::{ - div, AnyElement, Bounds, Component, Div, DivState, Element, ElementId, ElementInteractivity, - FocusListeners, Focusable, FocusableKeyDispatch, KeyDispatch, LayoutId, - NonFocusableKeyDispatch, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, - StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, + AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent, + InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, + Styled, ViewContext, }; use util::ResultExt; -pub struct Svg< - V: 'static, - I: ElementInteractivity = StatelessInteractivity, - F: KeyDispatch = NonFocusableKeyDispatch, -> { - base: Div, +pub struct Svg { + interactivity: Interactivity, path: Option, } -pub fn svg() -> Svg, NonFocusableKeyDispatch> { +pub fn svg() -> Svg { Svg { - base: div(), + interactivity: Interactivity::default(), path: None, } } -impl Svg -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Svg { pub fn path(mut self, path: impl Into) -> Self { self.path = Some(path.into()); self } } -impl Svg, F> -where - F: KeyDispatch, -{ - pub fn id(self, id: impl Into) -> Svg, F> { - Svg { - base: self.base.id(id), - path: self.path, - } - } -} - -impl Component for Svg -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Component for Svg { fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Svg -where - I: ElementInteractivity, - F: KeyDispatch, -{ - type ElementState = DivState; +impl Element for Svg { + type ElementState = InteractiveElementState; - fn id(&self) -> Option { - self.base.id() + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() } fn initialize( @@ -72,7 +43,7 @@ where element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - self.base.initialize(view_state, element_state, cx) + self.interactivity.initialize(element_state, cx) } fn layout( @@ -81,7 +52,9 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - self.base.layout(view_state, element_state, cx) + self.interactivity.layout(element_state, cx, |style, cx| { + cx.request_layout(&style, None) + }) } fn paint( @@ -93,65 +66,23 @@ where ) where Self: Sized, { - self.base.paint(bounds, view, element_state, cx); - let color = self - .base - .compute_style(bounds, element_state, cx) - .text - .color; - if let Some((path, color)) = self.path.as_ref().zip(color) { - cx.paint_svg(bounds, path.clone(), color).log_err(); - } + self.interactivity + .paint(bounds, bounds.size, element_state, cx, |style, _, cx| { + if let Some((path, color)) = self.path.as_ref().zip(style.text.color) { + cx.paint_svg(bounds, path.clone(), color).log_err(); + } + }) } } -impl Styled for Svg -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Styled for Svg { fn style(&mut self) -> &mut StyleRefinement { - self.base.style() + &mut self.interactivity.base_style } } -impl StatelessInteractive for Svg -where - I: ElementInteractivity, - F: KeyDispatch, -{ - fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { - self.base.stateless_interactivity() - } -} - -impl StatefulInteractive for Svg, F> -where - V: 'static, - F: KeyDispatch, -{ - fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { - self.base.stateful_interactivity() - } -} - -impl Focusable for Svg> -where - I: ElementInteractivity, -{ - fn focus_listeners(&mut self) -> &mut FocusListeners { - self.base.focus_listeners() - } - - fn set_focus_style(&mut self, style: StyleRefinement) { - self.base.set_focus_style(style) - } - - fn set_focus_in_style(&mut self, style: StyleRefinement) { - self.base.set_focus_in_style(style) - } - - fn set_in_focus_style(&mut self, style: StyleRefinement) { - self.base.set_in_focus_style(style) +impl InteractiveComponent for Svg { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity } } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 5c5709d32e..8688581cb8 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -53,7 +53,7 @@ impl Component for Text { impl Element for Text { type ElementState = Arc>>; - fn id(&self) -> Option { + fn element_id(&self) -> Option { None } diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 521df8699a..2f6584cda5 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,24 +1,23 @@ use crate::{ point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, - ElementId, ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size, - StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity, - StyleRefinement, Styled, ViewContext, + ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels, + Point, Size, StyleRefinement, Styled, ViewContext, }; use parking_lot::Mutex; use smallvec::SmallVec; -use std::{cmp, ops::Range, sync::Arc}; +use std::{cmp, mem, ops::Range, sync::Arc}; use taffy::style::Overflow; /// uniform_list provides lazy rendering for a set of items that are of uniform height. /// When rendered into a container with overflow-y: hidden and a fixed (or max) height, /// uniform_list will only render the visibile subset of items. -pub fn uniform_list( - id: Id, +pub fn uniform_list( + id: I, item_count: usize, f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> SmallVec<[C; 64]>, ) -> UniformList where - Id: Into, + I: Into, V: 'static, C: Component, { @@ -37,7 +36,10 @@ where .map(|component| component.render()) .collect() }), - interactivity: StatefulInteractivity::new(id, StatelessInteractivity::default()), + interactivity: Interactivity { + element_id: Some(id.into()), + ..Default::default() + }, scroll_handle: None, } } @@ -54,7 +56,7 @@ pub struct UniformList { &'a mut ViewContext, ) -> SmallVec<[AnyElement; 64]>, >, - interactivity: StatefulInteractivity, + interactivity: Interactivity, scroll_handle: Option, } @@ -103,7 +105,7 @@ pub struct UniformListState { impl Element for UniformList { type ElementState = UniformListState; - fn id(&self) -> Option { + fn element_id(&self) -> Option { Some(self.id.clone()) } @@ -113,13 +115,18 @@ impl Element for UniformList { element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - element_state.unwrap_or_else(|| { + if let Some(mut element_state) = element_state { + element_state.interactive = self + .interactivity + .initialize(Some(element_state.interactive), cx); + element_state + } else { let item_size = self.measure_item(view_state, None, cx); UniformListState { - interactive: InteractiveElementState::default(), + interactive: self.interactivity.initialize(None, cx), item_size, } - }) + } } fn layout( @@ -132,35 +139,44 @@ impl Element for UniformList { let item_size = element_state.item_size; let rem_size = cx.rem_size(); - cx.request_measured_layout( - self.computed_style(), - rem_size, - move |known_dimensions: Size>, available_space: Size| { - let desired_height = item_size.height * max_items; - let width = known_dimensions - .width - .unwrap_or(match available_space.width { - AvailableSpace::Definite(x) => x, - AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width, - }); - let height = match available_space.height { - AvailableSpace::Definite(x) => desired_height.min(x), - AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height, - }; - size(width, height) - }, - ) + self.interactivity + .layout(&mut element_state.interactive, cx, |style, cx| { + cx.request_measured_layout( + style, + rem_size, + move |known_dimensions: Size>, + available_space: Size| { + let desired_height = item_size.height * max_items; + let width = known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + item_size.width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(x) => desired_height.min(x), + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + desired_height + } + }; + size(width, height) + }, + ) + }) } fn paint( &mut self, - bounds: crate::Bounds, + bounds: Bounds, view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - let style = self.computed_style(); - + let style = + self.interactivity + .compute_style(Some(bounds), &mut element_state.interactive, cx); let border = style.border_widths.to_pixels(cx.rem_size()); let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); @@ -170,74 +186,75 @@ impl Element for UniformList { - point(border.right + padding.right, border.bottom + padding.bottom), ); - cx.with_z_index(style.z_index.unwrap_or(0), |cx| { - style.paint(bounds, cx); + let item_size = element_state.item_size; + let content_size = Size { + width: padded_bounds.size.width, + height: item_size.height * self.item_count, + }; - let content_size; - if self.item_count > 0 { - let item_height = self - .measure_item(view_state, Some(padded_bounds.size.width), cx) - .height; - if let Some(scroll_handle) = self.scroll_handle.clone() { - scroll_handle.0.lock().replace(ScrollHandleState { - item_height, - list_height: padded_bounds.size.height, - scroll_offset: element_state.interactive.track_scroll_offset(), - }); - } - let visible_item_count = if item_height > px(0.) { - (padded_bounds.size.height / item_height).ceil() as usize + 1 - } else { - 0 - }; - let scroll_offset = element_state - .interactive - .scroll_offset() - .map_or((0.0).into(), |offset| offset.y); - let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize; - let visible_range = first_visible_element_ix - ..cmp::min( - first_visible_element_ix + visible_item_count, - self.item_count, - ); + let mut interactivity = mem::take(&mut self.interactivity); + let shared_scroll_offset = element_state.interactive.scroll_offset.clone().unwrap(); - let mut items = (self.render_items)(view_state, visible_range.clone(), cx); + interactivity.paint( + bounds, + content_size, + &mut element_state.interactive, + cx, + |style, scroll_offset, cx| { + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); - content_size = Size { - width: padded_bounds.size.width, - height: item_height * self.item_count, - }; - - cx.with_z_index(1, |cx| { - for (item, ix) in items.iter_mut().zip(visible_range) { - let item_origin = - padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset); - let available_space = size( - AvailableSpace::Definite(padded_bounds.size.width), - AvailableSpace::Definite(item_height), - ); - item.draw(item_origin, available_space, view_state, cx); - } - }); - } else { - content_size = Size { - width: bounds.size.width, - height: px(0.), - }; - } - - let overflow = point(style.overflow.x, Overflow::Scroll); - - cx.with_z_index(0, |cx| { - self.interactivity.handle_events( - bounds, - content_size, - overflow, - &mut element_state.interactive, - cx, + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top + padding.top), + bounds.lower_right() + - point(border.right + padding.right, border.bottom + padding.bottom), ); - }); - }) + + cx.with_z_index(style.z_index.unwrap_or(0), |cx| { + style.paint(bounds, cx); + + if self.item_count > 0 { + let item_height = self + .measure_item(view_state, Some(padded_bounds.size.width), cx) + .height; + if let Some(scroll_handle) = self.scroll_handle.clone() { + scroll_handle.0.lock().replace(ScrollHandleState { + item_height, + list_height: padded_bounds.size.height, + scroll_offset: shared_scroll_offset, + }); + } + let visible_item_count = if item_height > px(0.) { + (padded_bounds.size.height / item_height).ceil() as usize + 1 + } else { + 0 + }; + + let first_visible_element_ix = + (-scroll_offset.y / item_height).floor() as usize; + let visible_range = first_visible_element_ix + ..cmp::min( + first_visible_element_ix + visible_item_count, + self.item_count, + ); + + let mut items = (self.render_items)(view_state, visible_range.clone(), cx); + cx.with_z_index(1, |cx| { + for (item, ix) in items.iter_mut().zip(visible_range) { + let item_origin = padded_bounds.origin + + point(px(0.), item_height * ix + scroll_offset.y); + let available_space = size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_height), + ); + item.draw(item_origin, available_space, view_state, cx); + } + }); + } + }) + }, + ); + self.interactivity = interactivity; } } @@ -275,14 +292,8 @@ impl UniformList { } } -impl StatelessInteractive for UniformList { - fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { - self.interactivity.as_stateless_mut() - } -} - -impl StatefulInteractive for UniformList { - fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { +impl InteractiveComponent for UniformList { + fn interactivity(&mut self) -> &mut crate::Interactivity { &mut self.interactivity } } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index f896bfc439..702a7ca5db 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,915 +1,17 @@ use crate::{ - div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, Bounds, Component, - DispatchPhase, Div, Element, ElementId, FocusHandle, KeyContext, Keystroke, Modifiers, - Overflow, Pixels, Point, Render, SharedString, Size, Style, StyleRefinement, Task, View, - ViewContext, + div, point, px, AnyDrag, AnyTooltip, AnyView, AppContext, Bounds, Component, DispatchPhase, + FocusHandle, Keystroke, Modifiers, Node, Pixels, Point, Render, SharedString, StyleRefinement, + Task, ViewContext, }; -use collections::HashMap; -use derive_more::{Deref, DerefMut}; -use parking_lot::Mutex; -use refineable::Refineable; use smallvec::SmallVec; use std::{ - any::{Any, TypeId}, - fmt::Debug, - marker::PhantomData, - mem, - ops::Deref, - path::PathBuf, - sync::Arc, - time::Duration, + any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf, sync::Arc, time::Duration, }; const DRAG_THRESHOLD: f64 = 2.; const TOOLTIP_DELAY: Duration = Duration::from_millis(500); const TOOLTIP_OFFSET: Point = Point::new(px(10.0), px(8.0)); -pub trait StatelessInteractive: Element { - fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity; - - fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.stateless_interactivity().hover_style = f(StyleRefinement::default()); - self - } - - fn group_hover( - mut self, - group_name: impl Into, - f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity().group_hover_style = Some(GroupStyle { - group: group_name.into(), - style: f(StyleRefinement::default()), - }); - self - } - - fn on_mouse_down( - mut self, - button: MouseButton, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_down_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble - && event.button == button - && bounds.contains_point(&event.position) - { - handler(view, event, cx) - } - })); - self - } - - fn on_mouse_up( - mut self, - button: MouseButton, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_up_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble - && event.button == button - && bounds.contains_point(&event.position) - { - handler(view, event, cx) - } - })); - self - } - - fn on_mouse_down_out( - mut self, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_down_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { - handler(view, event, cx) - } - })); - self - } - - fn on_mouse_up_out( - mut self, - button: MouseButton, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_up_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Capture - && event.button == button - && !bounds.contains_point(&event.position) - { - handler(view, event, cx); - } - })); - self - } - - fn on_mouse_move( - mut self, - handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_move_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx); - } - })); - self - } - - fn on_scroll_wheel( - mut self, - handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .scroll_wheel_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx); - } - })); - self - } - - /// Capture the given action, fires during the capture phase - fn capture_action( - mut self, - listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity().action_listeners.push(( - TypeId::of::(), - Box::new(move |view, action, phase, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Capture { - listener(view, action, cx) - } - }), - )); - self - } - - /// Add a listener for the given action, fires during the bubble event phase - fn on_action( - mut self, - listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity().action_listeners.push(( - TypeId::of::(), - Box::new(move |view, action, phase, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Bubble { - listener(view, action, cx) - } - }), - )); - self - } - - fn on_key_down( - mut self, - listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .key_down_listeners - .push(Box::new(move |view, event, phase, cx| { - listener(view, event, phase, cx) - })); - self - } - - fn on_key_up( - mut self, - listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .key_up_listeners - .push(Box::new(move |view, event, phase, cx| { - listener(view, event, phase, cx) - })); - self - } - - fn drag_over(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .drag_over_styles - .push((TypeId::of::(), f(StyleRefinement::default()))); - self - } - - fn group_drag_over( - mut self, - group_name: impl Into, - f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity().group_drag_over_styles.push(( - TypeId::of::(), - GroupStyle { - group: group_name.into(), - style: f(StyleRefinement::default()), - }, - )); - self - } - - fn on_drop( - mut self, - listener: impl Fn(&mut V, View, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity().drop_listeners.push(( - TypeId::of::(), - Box::new(move |view, dragged_view, cx| { - listener(view, dragged_view.downcast().unwrap(), cx); - }), - )); - self - } -} - -pub trait StatefulInteractive: StatelessInteractive { - fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity; - - fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.stateful_interactivity().active_style = f(StyleRefinement::default()); - self - } - - fn group_active( - mut self, - group_name: impl Into, - f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self - where - Self: Sized, - { - self.stateful_interactivity().group_active_style = Some(GroupStyle { - group: group_name.into(), - style: f(StyleRefinement::default()), - }); - self - } - - fn on_click( - mut self, - listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateful_interactivity() - .click_listeners - .push(Box::new(move |view, event, cx| listener(view, event, cx))); - self - } - - fn on_drag( - mut self, - listener: impl Fn(&mut V, &mut ViewContext) -> View + 'static, - ) -> Self - where - Self: Sized, - W: 'static + Render, - { - debug_assert!( - self.stateful_interactivity().drag_listener.is_none(), - "calling on_drag more than once on the same element is not supported" - ); - self.stateful_interactivity().drag_listener = - Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag { - view: listener(view_state, cx).into(), - cursor_offset, - })); - self - } - - fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext)) -> Self - where - Self: Sized, - { - debug_assert!( - self.stateful_interactivity().hover_listener.is_none(), - "calling on_hover more than once on the same element is not supported" - ); - self.stateful_interactivity().hover_listener = Some(Box::new(listener)); - self - } - - fn tooltip( - mut self, - build_tooltip: impl Fn(&mut V, &mut ViewContext) -> View + 'static, - ) -> Self - where - Self: Sized, - W: 'static + Render, - { - debug_assert!( - self.stateful_interactivity().tooltip_builder.is_none(), - "calling tooltip more than once on the same element is not supported" - ); - self.stateful_interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| { - build_tooltip(view_state, cx).into() - })); - - self - } -} - -pub trait ElementInteractivity: 'static { - fn as_stateless(&self) -> &StatelessInteractivity; - fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity; - fn as_stateful(&self) -> Option<&StatefulInteractivity>; - fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity>; - - fn refine_style( - &self, - style: &mut Style, - bounds: Bounds, - element_state: &InteractiveElementState, - cx: &mut ViewContext, - ) { - let mouse_position = cx.mouse_position(); - let stateless = self.as_stateless(); - if let Some(group_hover) = stateless.group_hover_style.as_ref() { - if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { - if group_bounds.contains_point(&mouse_position) { - style.refine(&group_hover.style); - } - } - } - if bounds.contains_point(&mouse_position) { - style.refine(&stateless.hover_style); - } - - if let Some(drag) = cx.active_drag.take() { - for (state_type, group_drag_style) in &self.as_stateless().group_drag_over_styles { - if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { - if *state_type == drag.view.entity_type() - && group_bounds.contains_point(&mouse_position) - { - style.refine(&group_drag_style.style); - } - } - } - - for (state_type, drag_over_style) in &self.as_stateless().drag_over_styles { - if *state_type == drag.view.entity_type() && bounds.contains_point(&mouse_position) - { - style.refine(drag_over_style); - } - } - - cx.active_drag = Some(drag); - } - - if let Some(stateful) = self.as_stateful() { - let active_state = element_state.active_state.lock(); - if active_state.group { - if let Some(group_style) = stateful.group_active_style.as_ref() { - style.refine(&group_style.style); - } - } - if active_state.element { - style.refine(&stateful.active_style); - } - } - } - - fn initialize(&mut self, cx: &mut ViewContext) { - let stateless = self.as_stateless_mut(); - - for listener in stateless.key_down_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { - listener(state, event, phase, cx); - }) - } - - for listener in stateless.key_up_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { - listener(state, event, phase, cx); - }) - } - - for (action_type, listener) in stateless.action_listeners.drain(..) { - cx.on_action(action_type, listener) - } - } - - fn handle_events( - &mut self, - bounds: Bounds, - content_size: Size, - overflow: Point, - element_state: &mut InteractiveElementState, - cx: &mut ViewContext, - ) { - let stateless = self.as_stateless_mut(); - for listener in stateless.mouse_down_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); - }) - } - - for listener in stateless.mouse_up_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); - }) - } - - for listener in stateless.mouse_move_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); - }) - } - - for listener in stateless.scroll_wheel_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); - }) - } - - let hover_group_bounds = stateless - .group_hover_style - .as_ref() - .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx)); - - if let Some(group_bounds) = hover_group_bounds { - let hovered = group_bounds.contains_point(&cx.mouse_position()); - cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture { - if group_bounds.contains_point(&event.position) != hovered { - cx.notify(); - } - } - }); - } - - if stateless.hover_style.is_some() - || (cx.active_drag.is_some() && !stateless.drag_over_styles.is_empty()) - { - let hovered = bounds.contains_point(&cx.mouse_position()); - cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture { - if bounds.contains_point(&event.position) != hovered { - cx.notify(); - } - } - }); - } - - if cx.active_drag.is_some() { - let drop_listeners = mem::take(&mut stateless.drop_listeners); - cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - if let Some(drag_state_type) = - cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) - { - for (drop_state_type, listener) in &drop_listeners { - if *drop_state_type == drag_state_type { - let drag = cx - .active_drag - .take() - .expect("checked for type drag state type above"); - listener(view, drag.view.clone(), cx); - cx.notify(); - cx.stop_propagation(); - } - } - } - } - }); - } - - if let Some(stateful) = self.as_stateful_mut() { - let click_listeners = mem::take(&mut stateful.click_listeners); - let drag_listener = mem::take(&mut stateful.drag_listener); - - if !click_listeners.is_empty() || drag_listener.is_some() { - let pending_mouse_down = element_state.pending_mouse_down.clone(); - let mouse_down = pending_mouse_down.lock().clone(); - if let Some(mouse_down) = mouse_down { - if let Some(drag_listener) = drag_listener { - let active_state = element_state.active_state.clone(); - - cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { - if cx.active_drag.is_some() { - if phase == DispatchPhase::Capture { - cx.notify(); - } - } else if phase == DispatchPhase::Bubble - && bounds.contains_point(&event.position) - && (event.position - mouse_down.position).magnitude() - > DRAG_THRESHOLD - { - *active_state.lock() = ActiveState::default(); - let cursor_offset = event.position - bounds.origin; - let drag = drag_listener(view_state, cursor_offset, cx); - cx.active_drag = Some(drag); - cx.notify(); - cx.stop_propagation(); - } - }); - } - - cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) - { - let mouse_click = ClickEvent { - down: mouse_down.clone(), - up: event.clone(), - }; - for listener in &click_listeners { - listener(view_state, &mouse_click, cx); - } - } - *pending_mouse_down.lock() = None; - }); - } else { - cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) - { - *pending_mouse_down.lock() = Some(event.clone()); - } - }); - } - } - - if let Some(hover_listener) = stateful.hover_listener.take() { - let was_hovered = element_state.hover_state.clone(); - let has_mouse_down = element_state.pending_mouse_down.clone(); - - cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { - if phase != DispatchPhase::Bubble { - return; - } - let is_hovered = - bounds.contains_point(&event.position) && has_mouse_down.lock().is_none(); - let mut was_hovered = was_hovered.lock(); - - if is_hovered != was_hovered.clone() { - *was_hovered = is_hovered; - drop(was_hovered); - - hover_listener(view_state, is_hovered, cx); - } - }); - } - - if let Some(tooltip_builder) = stateful.tooltip_builder.take() { - let active_tooltip = element_state.active_tooltip.clone(); - let pending_mouse_down = element_state.pending_mouse_down.clone(); - - cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { - if phase != DispatchPhase::Bubble { - return; - } - - let is_hovered = bounds.contains_point(&event.position) - && pending_mouse_down.lock().is_none(); - if !is_hovered { - active_tooltip.lock().take(); - return; - } - - if active_tooltip.lock().is_none() { - let task = cx.spawn({ - let active_tooltip = active_tooltip.clone(); - let tooltip_builder = tooltip_builder.clone(); - - move |view, mut cx| async move { - cx.background_executor().timer(TOOLTIP_DELAY).await; - view.update(&mut cx, move |view_state, cx| { - active_tooltip.lock().replace(ActiveTooltip { - waiting: None, - tooltip: Some(AnyTooltip { - view: tooltip_builder(view_state, cx), - cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET, - }), - }); - cx.notify(); - }) - .ok(); - } - }); - active_tooltip.lock().replace(ActiveTooltip { - waiting: Some(task), - tooltip: None, - }); - } - }); - - if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { - if active_tooltip.tooltip.is_some() { - cx.active_tooltip = active_tooltip.tooltip.clone() - } - } - } - - let active_state = element_state.active_state.clone(); - if active_state.lock().is_none() { - let active_group_bounds = stateful - .group_active_style - .as_ref() - .and_then(|group_active| GroupBounds::get(&group_active.group, cx)); - cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble { - let group = active_group_bounds - .map_or(false, |bounds| bounds.contains_point(&down.position)); - let element = bounds.contains_point(&down.position); - if group || element { - *active_state.lock() = ActiveState { group, element }; - cx.notify(); - } - } - }); - } else { - cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Capture { - *active_state.lock() = ActiveState::default(); - cx.notify(); - } - }); - } - - if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll { - let scroll_offset = element_state - .scroll_offset - .get_or_insert_with(Arc::default) - .clone(); - let line_height = cx.line_height(); - let scroll_max = (content_size - bounds.size).max(&Size::default()); - - cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - let mut scroll_offset = scroll_offset.lock(); - let old_scroll_offset = *scroll_offset; - let delta = event.delta.pixel_delta(line_height); - - if overflow.x == Overflow::Scroll { - scroll_offset.x = - (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.)); - } - - if overflow.y == Overflow::Scroll { - scroll_offset.y = - (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.)); - } - - if *scroll_offset != old_scroll_offset { - cx.notify(); - cx.stop_propagation(); - } - } - }); - } - } - } -} - -#[derive(Deref, DerefMut)] -pub struct StatefulInteractivity { - pub id: ElementId, - #[deref] - #[deref_mut] - stateless: StatelessInteractivity, - click_listeners: SmallVec<[ClickListener; 2]>, - active_style: StyleRefinement, - group_active_style: Option, - drag_listener: Option>, - hover_listener: Option>, - tooltip_builder: Option>, -} - -impl StatefulInteractivity { - pub fn new(id: ElementId, stateless: StatelessInteractivity) -> Self { - Self { - id, - stateless, - click_listeners: SmallVec::new(), - active_style: StyleRefinement::default(), - group_active_style: None, - drag_listener: None, - hover_listener: None, - tooltip_builder: None, - } - } -} - -impl ElementInteractivity for StatefulInteractivity { - fn as_stateful(&self) -> Option<&StatefulInteractivity> { - Some(self) - } - - fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity> { - Some(self) - } - - fn as_stateless(&self) -> &StatelessInteractivity { - &self.stateless - } - - fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity { - &mut self.stateless - } -} - -type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; - -pub struct StatelessInteractivity { - pub dispatch_context: KeyContext, - pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, - pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, - pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, - pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, - pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, - pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, - pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, - pub hover_style: StyleRefinement, - pub group_hover_style: Option, - drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, - group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, - drop_listeners: SmallVec<[(TypeId, Box>); 2]>, -} - -impl StatelessInteractivity { - pub fn into_stateful(self, id: impl Into) -> StatefulInteractivity { - StatefulInteractivity { - id: id.into(), - stateless: self, - click_listeners: SmallVec::new(), - drag_listener: None, - hover_listener: None, - tooltip_builder: None, - active_style: StyleRefinement::default(), - group_active_style: None, - } - } -} - -pub struct GroupStyle { - pub group: SharedString, - pub style: StyleRefinement, -} - -#[derive(Default)] -pub struct GroupBounds(HashMap; 1]>>); - -impl GroupBounds { - pub fn get(name: &SharedString, cx: &mut AppContext) -> Option> { - cx.default_global::() - .0 - .get(name) - .and_then(|bounds_stack| bounds_stack.last()) - .cloned() - } - - pub fn push(name: SharedString, bounds: Bounds, cx: &mut AppContext) { - cx.default_global::() - .0 - .entry(name) - .or_default() - .push(bounds); - } - - pub fn pop(name: &SharedString, cx: &mut AppContext) { - cx.default_global::().0.get_mut(name).unwrap().pop(); - } -} - -#[derive(Copy, Clone, Default, Eq, PartialEq)] -struct ActiveState { - pub group: bool, - pub element: bool, -} - -impl ActiveState { - pub fn is_none(&self) -> bool { - !self.group && !self.element - } -} - -#[derive(Default)] -pub struct InteractiveElementState { - active_state: Arc>, - hover_state: Arc>, - pending_mouse_down: Arc>>, - scroll_offset: Option>>>, - active_tooltip: Arc>>, -} - -struct ActiveTooltip { - #[allow(unused)] // used to drop the task - waiting: Option>, - tooltip: Option, -} - -impl InteractiveElementState { - pub fn scroll_offset(&self) -> Option> { - self.scroll_offset - .as_ref() - .map(|offset| offset.lock().clone()) - } - - pub fn track_scroll_offset(&mut self) -> Arc>> { - self.scroll_offset - .get_or_insert_with(|| Arc::new(Mutex::new(Default::default()))) - .clone() - } -} - -impl Default for StatelessInteractivity { - fn default() -> Self { - Self { - dispatch_context: KeyContext::default(), - mouse_down_listeners: SmallVec::new(), - mouse_up_listeners: SmallVec::new(), - mouse_move_listeners: SmallVec::new(), - scroll_wheel_listeners: SmallVec::new(), - key_down_listeners: SmallVec::new(), - key_up_listeners: SmallVec::new(), - action_listeners: SmallVec::new(), - hover_style: StyleRefinement::default(), - group_hover_style: None, - drag_over_styles: SmallVec::new(), - group_drag_over_styles: SmallVec::new(), - drop_listeners: SmallVec::new(), - } - } -} - -impl ElementInteractivity for StatelessInteractivity { - fn as_stateful(&self) -> Option<&StatefulInteractivity> { - None - } - - fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity> { - None - } - - fn as_stateless(&self) -> &StatelessInteractivity { - self - } - - fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity { - self - } -} - #[derive(Clone, Debug, Eq, PartialEq)] pub struct KeyDownEvent { pub keystroke: Keystroke, @@ -991,10 +93,6 @@ where } } -// impl Render for Drag { -// // fn render(&mut self, cx: ViewContext) -> -// } - #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] pub enum MouseButton { Left, @@ -1103,7 +201,7 @@ impl Deref for MouseExitEvent { pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); impl Render for ExternalPaths { - type Element = Div; + type Element = Node; fn render(&mut self, _: &mut ViewContext) -> Self::Element { div() // Intentionally left empty because the platform will render icons for the dragged files @@ -1229,8 +327,8 @@ pub type ActionListener = #[cfg(test)] mod test { use crate::{ - self as gpui, div, Div, FocusHandle, KeyBinding, Keystroke, ParentElement, Render, - StatefulInteractivity, StatelessInteractive, TestAppContext, VisualContext, + self as gpui, div, FocusHandle, InteractiveComponent, KeyBinding, Keystroke, Node, + ParentComponent, Render, Stateful, TestAppContext, VisualContext, }; struct TestView { @@ -1242,12 +340,12 @@ mod test { actions!(TestAction); impl Render for TestView { - type Element = Div>; + type Element = Stateful>; fn render(&mut self, _: &mut gpui::ViewContext) -> Self::Element { div().id("testview").child( div() - .context("test") + .key_context("test") .track_focus(&self.focus_handle) .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true) .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true), diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 8ace4188ae..afe0b0f3e2 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -1,7 +1,7 @@ use crate::{ - build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, - FocusId, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, Pixels, - Style, StyleRefinement, ViewContext, WindowContext, + build_action_from_type, Action, Bounds, DispatchPhase, FocusEvent, FocusHandle, FocusId, + KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, Pixels, Style, + StyleRefinement, ViewContext, WindowContext, }; use collections::HashMap; use parking_lot::Mutex; @@ -342,115 +342,3 @@ impl KeyDispatch for NonFocusableKeyDispatch { &mut self.key_context } } - -pub trait Focusable: Element { - fn focus_listeners(&mut self) -> &mut FocusListeners; - fn set_focus_style(&mut self, style: StyleRefinement); - fn set_focus_in_style(&mut self, style: StyleRefinement); - fn set_in_focus_style(&mut self, style: StyleRefinement); - - fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.set_focus_style(f(StyleRefinement::default())); - self - } - - fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.set_focus_in_style(f(StyleRefinement::default())); - self - } - - fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.set_in_focus_style(f(StyleRefinement::default())); - self - } - - fn on_focus( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - if event.focused.as_ref() == Some(focus_handle) { - listener(view, event, cx) - } - })); - self - } - - fn on_blur( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - if event.blurred.as_ref() == Some(focus_handle) { - listener(view, event, cx) - } - })); - self - } - - fn on_focus_in( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - let descendant_blurred = event - .blurred - .as_ref() - .map_or(false, |blurred| focus_handle.contains(blurred, cx)); - let descendant_focused = event - .focused - .as_ref() - .map_or(false, |focused| focus_handle.contains(focused, cx)); - - if !descendant_blurred && descendant_focused { - listener(view, event, cx) - } - })); - self - } - - fn on_focus_out( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - let descendant_blurred = event - .blurred - .as_ref() - .map_or(false, |blurred| focus_handle.contains(blurred, cx)); - let descendant_focused = event - .focused - .as_ref() - .map_or(false, |focused| focus_handle.contains(focused, cx)); - if descendant_blurred && !descendant_focused { - listener(view, event, cx) - } - })); - self - } -} diff --git a/crates/gpui2/src/prelude.rs b/crates/gpui2/src/prelude.rs index bc998fc1f4..7c2ad3f07f 100644 --- a/crates/gpui2/src/prelude.rs +++ b/crates/gpui2/src/prelude.rs @@ -1 +1,4 @@ -pub use crate::{Context, ParentElement, Refineable}; +pub use crate::{ + BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent, + ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext, +}; diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index 3f1fa843f9..71e38efd9b 100644 --- a/crates/gpui2/src/styled.rs +++ b/crates/gpui2/src/styled.rs @@ -6,21 +6,20 @@ use crate::{ use crate::{BoxShadow, TextStyleRefinement}; use refineable::Refineable; use smallvec::{smallvec, SmallVec}; +use taffy::style::Overflow; -pub trait Styled { +pub trait Styled: Sized { fn style(&mut self) -> &mut StyleRefinement; - fn computed_style(&mut self) -> Style { - Style::default().refined(self.style().clone()) - } - gpui2_macros::style_helpers!(); + fn z_index(mut self, z_index: u32) -> Self { + self.style().z_index = Some(z_index); + self + } + /// Sets the size of the element to the full width and height. - fn full(mut self) -> Self - where - Self: Sized, - { + fn full(mut self) -> Self { self.style().size.width = Some(relative(1.).into()); self.style().size.height = Some(relative(1.).into()); self @@ -28,118 +27,98 @@ pub trait Styled { /// Sets the position of the element to `relative`. /// [Docs](https://tailwindcss.com/docs/position) - fn relative(mut self) -> Self - where - Self: Sized, - { + fn relative(mut self) -> Self { self.style().position = Some(Position::Relative); self } /// Sets the position of the element to `absolute`. /// [Docs](https://tailwindcss.com/docs/position) - fn absolute(mut self) -> Self - where - Self: Sized, - { + fn absolute(mut self) -> Self { self.style().position = Some(Position::Absolute); self } /// Sets the display type of the element to `block`. /// [Docs](https://tailwindcss.com/docs/display) - fn block(mut self) -> Self - where - Self: Sized, - { + fn block(mut self) -> Self { self.style().display = Some(Display::Block); self } /// Sets the display type of the element to `flex`. /// [Docs](https://tailwindcss.com/docs/display) - fn flex(mut self) -> Self - where - Self: Sized, - { + fn flex(mut self) -> Self { self.style().display = Some(Display::Flex); self } /// Sets the visibility of the element to `visible`. /// [Docs](https://tailwindcss.com/docs/visibility) - fn visible(mut self) -> Self - where - Self: Sized, - { + fn visible(mut self) -> Self { self.style().visibility = Some(Visibility::Visible); self } /// Sets the visibility of the element to `hidden`. /// [Docs](https://tailwindcss.com/docs/visibility) - fn invisible(mut self) -> Self - where - Self: Sized, - { + fn invisible(mut self) -> Self { self.style().visibility = Some(Visibility::Hidden); self } - fn cursor(mut self, cursor: CursorStyle) -> Self - where - Self: Sized, - { + fn overflow_hidden(mut self) -> Self { + self.style().overflow.x = Some(Overflow::Hidden); + self.style().overflow.y = Some(Overflow::Hidden); + self + } + + fn overflow_hidden_x(mut self) -> Self { + self.style().overflow.x = Some(Overflow::Hidden); + self + } + + fn overflow_hidden_y(mut self) -> Self { + self.style().overflow.y = Some(Overflow::Hidden); + self + } + + fn cursor(mut self, cursor: CursorStyle) -> Self { self.style().mouse_cursor = Some(cursor); self } /// Sets the cursor style when hovering an element to `default`. /// [Docs](https://tailwindcss.com/docs/cursor) - fn cursor_default(mut self) -> Self - where - Self: Sized, - { + fn cursor_default(mut self) -> Self { self.style().mouse_cursor = Some(CursorStyle::Arrow); self } /// Sets the cursor style when hovering an element to `pointer`. /// [Docs](https://tailwindcss.com/docs/cursor) - fn cursor_pointer(mut self) -> Self - where - Self: Sized, - { + fn cursor_pointer(mut self) -> Self { self.style().mouse_cursor = Some(CursorStyle::PointingHand); self } /// Sets the flex direction of the element to `column`. /// [Docs](https://tailwindcss.com/docs/flex-direction#column) - fn flex_col(mut self) -> Self - where - Self: Sized, - { + fn flex_col(mut self) -> Self { self.style().flex_direction = Some(FlexDirection::Column); self } /// Sets the flex direction of the element to `row`. /// [Docs](https://tailwindcss.com/docs/flex-direction#row) - fn flex_row(mut self) -> Self - where - Self: Sized, - { + fn flex_row(mut self) -> Self { self.style().flex_direction = Some(FlexDirection::Row); self } /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size. /// [Docs](https://tailwindcss.com/docs/flex#flex-1) - fn flex_1(mut self) -> Self - where - Self: Sized, - { + fn flex_1(mut self) -> Self { self.style().flex_grow = Some(1.); self.style().flex_shrink = Some(1.); self.style().flex_basis = Some(relative(0.).into()); @@ -148,10 +127,7 @@ pub trait Styled { /// Sets the element to allow a flex item to grow and shrink, taking into account its initial size. /// [Docs](https://tailwindcss.com/docs/flex#auto) - fn flex_auto(mut self) -> Self - where - Self: Sized, - { + fn flex_auto(mut self) -> Self { self.style().flex_grow = Some(1.); self.style().flex_shrink = Some(1.); self.style().flex_basis = Some(Length::Auto); @@ -160,10 +136,7 @@ pub trait Styled { /// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size. /// [Docs](https://tailwindcss.com/docs/flex#initial) - fn flex_initial(mut self) -> Self - where - Self: Sized, - { + fn flex_initial(mut self) -> Self { self.style().flex_grow = Some(0.); self.style().flex_shrink = Some(1.); self.style().flex_basis = Some(Length::Auto); @@ -172,10 +145,7 @@ pub trait Styled { /// Sets the element to prevent a flex item from growing or shrinking. /// [Docs](https://tailwindcss.com/docs/flex#none) - fn flex_none(mut self) -> Self - where - Self: Sized, - { + fn flex_none(mut self) -> Self { self.style().flex_grow = Some(0.); self.style().flex_shrink = Some(0.); self @@ -183,40 +153,28 @@ pub trait Styled { /// Sets the element to allow a flex item to grow to fill any available space. /// [Docs](https://tailwindcss.com/docs/flex-grow) - fn grow(mut self) -> Self - where - Self: Sized, - { + fn grow(mut self) -> Self { self.style().flex_grow = Some(1.); self } /// Sets the element to align flex items to the start of the container's cross axis. /// [Docs](https://tailwindcss.com/docs/align-items#start) - fn items_start(mut self) -> Self - where - Self: Sized, - { + fn items_start(mut self) -> Self { self.style().align_items = Some(AlignItems::FlexStart); self } /// Sets the element to align flex items to the end of the container's cross axis. /// [Docs](https://tailwindcss.com/docs/align-items#end) - fn items_end(mut self) -> Self - where - Self: Sized, - { + fn items_end(mut self) -> Self { self.style().align_items = Some(AlignItems::FlexEnd); self } /// Sets the element to align flex items along the center of the container's cross axis. /// [Docs](https://tailwindcss.com/docs/align-items#center) - fn items_center(mut self) -> Self - where - Self: Sized, - { + fn items_center(mut self) -> Self { self.style().align_items = Some(AlignItems::Center); self } @@ -224,40 +182,28 @@ pub trait Styled { /// Sets the element to justify flex items along the container's main axis /// such that there is an equal amount of space between each item. /// [Docs](https://tailwindcss.com/docs/justify-content#space-between) - fn justify_between(mut self) -> Self - where - Self: Sized, - { + fn justify_between(mut self) -> Self { self.style().justify_content = Some(JustifyContent::SpaceBetween); self } /// Sets the element to justify flex items along the center of the container's main axis. /// [Docs](https://tailwindcss.com/docs/justify-content#center) - fn justify_center(mut self) -> Self - where - Self: Sized, - { + fn justify_center(mut self) -> Self { self.style().justify_content = Some(JustifyContent::Center); self } /// Sets the element to justify flex items against the start of the container's main axis. /// [Docs](https://tailwindcss.com/docs/justify-content#start) - fn justify_start(mut self) -> Self - where - Self: Sized, - { + fn justify_start(mut self) -> Self { self.style().justify_content = Some(JustifyContent::Start); self } /// Sets the element to justify flex items against the end of the container's main axis. /// [Docs](https://tailwindcss.com/docs/justify-content#end) - fn justify_end(mut self) -> Self - where - Self: Sized, - { + fn justify_end(mut self) -> Self { self.style().justify_content = Some(JustifyContent::End); self } @@ -265,10 +211,7 @@ pub trait Styled { /// Sets the element to justify items along the container's main axis such /// that there is an equal amount of space on each side of each item. /// [Docs](https://tailwindcss.com/docs/justify-content#space-around) - fn justify_around(mut self) -> Self - where - Self: Sized, - { + fn justify_around(mut self) -> Self { self.style().justify_content = Some(JustifyContent::SpaceAround); self } @@ -295,30 +238,21 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self - where - Self: Sized, - { + fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self { self.style().box_shadow = Some(shadows); self } /// Clears the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_none(mut self) -> Self - where - Self: Sized, - { + fn shadow_none(mut self) -> Self { self.style().box_shadow = Some(Default::default()); self } /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_sm(mut self) -> Self - where - Self: Sized, - { + fn shadow_sm(mut self) -> Self { self.style().box_shadow = Some(smallvec::smallvec![BoxShadow { color: hsla(0., 0., 0., 0.05), offset: point(px(0.), px(1.)), @@ -330,10 +264,7 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_md(mut self) -> Self - where - Self: Sized, - { + fn shadow_md(mut self) -> Self { self.style().box_shadow = Some(smallvec![ BoxShadow { color: hsla(0.5, 0., 0., 0.1), @@ -353,10 +284,7 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_lg(mut self) -> Self - where - Self: Sized, - { + fn shadow_lg(mut self) -> Self { self.style().box_shadow = Some(smallvec![ BoxShadow { color: hsla(0., 0., 0., 0.1), @@ -376,10 +304,7 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_xl(mut self) -> Self - where - Self: Sized, - { + fn shadow_xl(mut self) -> Self { self.style().box_shadow = Some(smallvec![ BoxShadow { color: hsla(0., 0., 0., 0.1), @@ -399,10 +324,7 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_2xl(mut self) -> Self - where - Self: Sized, - { + fn shadow_2xl(mut self) -> Self { self.style().box_shadow = Some(smallvec![BoxShadow { color: hsla(0., 0., 0., 0.25), offset: point(px(0.), px(25.)), @@ -417,198 +339,138 @@ pub trait Styled { &mut style.text } - fn text_color(mut self, color: impl Into) -> Self - where - Self: Sized, - { + fn text_color(mut self, color: impl Into) -> Self { self.text_style().get_or_insert_with(Default::default).color = Some(color.into()); self } - fn text_size(mut self, size: impl Into) -> Self - where - Self: Sized, - { + fn text_size(mut self, size: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(size.into()); self } - fn text_xs(mut self) -> Self - where - Self: Sized, - { + fn text_xs(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(0.75).into()); self } - fn text_sm(mut self) -> Self - where - Self: Sized, - { + fn text_sm(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(0.875).into()); self } - fn text_base(mut self) -> Self - where - Self: Sized, - { + fn text_base(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(1.0).into()); self } - fn text_lg(mut self) -> Self - where - Self: Sized, - { + fn text_lg(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(1.125).into()); self } - fn text_xl(mut self) -> Self - where - Self: Sized, - { + fn text_xl(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(1.25).into()); self } - fn text_2xl(mut self) -> Self - where - Self: Sized, - { + fn text_2xl(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(1.5).into()); self } - fn text_3xl(mut self) -> Self - where - Self: Sized, - { + fn text_3xl(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(1.875).into()); self } - fn text_decoration_none(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_none(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .underline = None; self } - fn text_decoration_color(mut self, color: impl Into) -> Self - where - Self: Sized, - { + fn text_decoration_color(mut self, color: impl Into) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.color = Some(color.into()); self } - fn text_decoration_solid(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_solid(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.wavy = false; self } - fn text_decoration_wavy(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_wavy(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.wavy = true; self } - fn text_decoration_0(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_0(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(0.); self } - fn text_decoration_1(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_1(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(1.); self } - fn text_decoration_2(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_2(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(2.); self } - fn text_decoration_4(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_4(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(4.); self } - fn text_decoration_8(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_8(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(8.); self } - fn font(mut self, family_name: impl Into) -> Self - where - Self: Sized, - { + fn font(mut self, family_name: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_family = Some(family_name.into()); self } - fn line_height(mut self, line_height: impl Into) -> Self - where - Self: Sized, - { + fn line_height(mut self, line_height: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) .line_height = Some(line_height.into()); diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 3299ce7d45..801d8ceb0b 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -206,7 +206,7 @@ impl From> for AnyView { impl Element for AnyView { type ElementState = Box; - fn id(&self) -> Option { + fn element_id(&self) -> Option { Some(self.model.entity_id.into()) } diff --git a/crates/gpui2_macros/src/style_helpers.rs b/crates/gpui2_macros/src/style_helpers.rs index 57aef03afa..181311807c 100644 --- a/crates/gpui2_macros/src/style_helpers.rs +++ b/crates/gpui2_macros/src/style_helpers.rs @@ -130,7 +130,7 @@ fn generate_predefined_setter( let method = quote! { #[doc = #doc_string] - fn #method_name(mut self) -> Self where Self: std::marker::Sized { + fn #method_name(mut self) -> Self { let style = self.style(); #(#field_assignments)* self @@ -163,7 +163,7 @@ fn generate_custom_value_setter( let method = quote! { #[doc = #doc_string] - fn #method_name(mut self, length: impl std::clone::Clone + Into) -> Self where Self: std::marker::Sized { + fn #method_name(mut self, length: impl std::clone::Clone + Into) -> Self { let style = self.style(); #(#field_assignments)* self diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index e1979f1b13..189c07b49a 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,6 +1,6 @@ use editor::Editor; use gpui::{ - div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled, Task, + div, uniform_list, Component, Node, ParentComponent, Render, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; use std::{cmp, sync::Arc}; @@ -139,11 +139,11 @@ impl Picker { } impl Render for Picker { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() - .context("picker") + .key_context("picker") .size_full() .elevation_2(cx) .on_action(Self::select_next) diff --git a/crates/storybook2/src/stories/colors.rs b/crates/storybook2/src/stories/colors.rs index 13b7b36a8c..a5e9efc0da 100644 --- a/crates/storybook2/src/stories/colors.rs +++ b/crates/storybook2/src/stories/colors.rs @@ -1,12 +1,12 @@ use crate::story::Story; -use gpui::{px, Div, Render}; +use gpui::{prelude::*, px, Node, Render}; use theme2::{default_color_scales, ColorScaleStep}; use ui::prelude::*; pub struct ColorsStory; impl Render for ColorsStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let color_scales = default_color_scales(); diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index bba798d9fe..a6b27c460d 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -1,6 +1,5 @@ use gpui::{ - actions, div, Div, FocusHandle, Focusable, FocusableKeyDispatch, KeyBinding, ParentElement, - Render, StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, + actions, div, prelude::*, FocusHandle, Focusable, KeyBinding, Node, Render, Stateful, View, WindowContext, }; use theme2::ActiveTheme; @@ -28,7 +27,7 @@ impl FocusStory { } impl Render for FocusStory { - type Element = Div, FocusableKeyDispatch>; + type Element = Focusable>>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); @@ -42,7 +41,7 @@ impl Render for FocusStory { div() .id("parent") .focusable() - .context("parent") + .key_context("parent") .on_action(|_, action: &ActionA, cx| { println!("Action A dispatched on parent"); }) @@ -62,7 +61,7 @@ impl Render for FocusStory { .child( div() .track_focus(&self.child_1_focus) - .context("child-1") + .key_context("child-1") .on_action(|_, action: &ActionB, cx| { println!("Action B dispatched on child 1 during"); }) @@ -82,7 +81,7 @@ impl Render for FocusStory { .child( div() .track_focus(&self.child_2_focus) - .context("child-2") + .key_context("child-2") .on_action(|_, action: &ActionC, cx| { println!("Action C dispatched on child 2"); }) diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index 6831ae2722..0a165eff74 100644 --- a/crates/storybook2/src/stories/kitchen_sink.rs +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -1,5 +1,5 @@ use crate::{story::Story, story_selector::ComponentStory}; -use gpui::{Div, Render, StatefulInteractivity, View, VisualContext}; +use gpui::{prelude::*, Node, Render, Stateful, View}; use strum::IntoEnumIterator; use ui::prelude::*; @@ -12,7 +12,7 @@ impl KitchenSinkStory { } impl Render for KitchenSinkStory { - type Element = Div>; + type Element = Stateful>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let component_stories = ComponentStory::iter() diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 067c190575..bba8ae9990 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,11 +1,7 @@ -use std::sync::Arc; - use fuzzy::StringMatchCandidate; -use gpui::{ - div, Component, Div, KeyBinding, ParentElement, Render, StatelessInteractive, Styled, Task, - View, VisualContext, WindowContext, -}; +use gpui::{div, prelude::*, KeyBinding, Node, Render, Styled, Task, View, WindowContext}; use picker::{Picker, PickerDelegate}; +use std::sync::Arc; use theme2::ActiveTheme; pub struct PickerStory { @@ -38,7 +34,7 @@ impl Delegate { } impl PickerDelegate for Delegate { - type ListItem = Div>; + type ListItem = Node>; fn match_count(&self) -> usize { self.candidates.len() @@ -207,7 +203,7 @@ impl PickerStory { } impl Render for PickerStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { div() diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index 296dc50cb4..c5675b5681 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -1,6 +1,5 @@ use gpui::{ - div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteractivity, Styled, - View, VisualContext, WindowContext, + div, prelude::*, px, Node, Render, SharedString, Stateful, Styled, View, WindowContext, }; use theme2::ActiveTheme; @@ -13,7 +12,7 @@ impl ScrollStory { } impl Render for ScrollStory { - type Element = Div>; + type Element = Stateful>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index b4a4c86e7e..86ab2fce99 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -1,4 +1,4 @@ -use gpui::{div, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext}; +use gpui::{div, white, Node, ParentComponent, Render, Styled, View, VisualContext, WindowContext}; pub struct TextStory; @@ -9,7 +9,7 @@ impl TextStory { } impl Render for TextStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { div().size_full().bg(white()).child(concat!( diff --git a/crates/storybook2/src/stories/z_index.rs b/crates/storybook2/src/stories/z_index.rs index 46ec0f4a35..259685b9fa 100644 --- a/crates/storybook2/src/stories/z_index.rs +++ b/crates/storybook2/src/stories/z_index.rs @@ -1,4 +1,4 @@ -use gpui::{px, rgb, Div, Hsla, Render}; +use gpui::{px, rgb, Hsla, Node, Render}; use ui::prelude::*; use crate::story::Story; @@ -8,7 +8,7 @@ use crate::story::Story; pub struct ZIndexStory; impl Render for ZIndexStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) @@ -77,7 +77,7 @@ trait Styles: Styled + Sized { } } -impl Styles for Div {} +impl Styles for Node {} #[derive(Component)] struct ZIndexExample { diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index f0ba124162..f20aa59095 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use clap::Parser; use gpui::{ - div, px, size, AnyView, AppContext, Bounds, Div, Render, ViewContext, VisualContext, + div, px, size, AnyView, AppContext, Bounds, Node, Render, ViewContext, VisualContext, WindowBounds, WindowOptions, }; use log::LevelFilter; @@ -107,7 +107,7 @@ impl StoryWrapper { } impl Render for StoryWrapper { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/theme2/src/players.rs b/crates/theme2/src/players.rs index 0e36ff5947..32b3504b65 100644 --- a/crates/theme2/src/players.rs +++ b/crates/theme2/src/players.rs @@ -40,12 +40,12 @@ pub use stories::*; mod stories { use super::*; use crate::{ActiveTheme, Story}; - use gpui::{div, img, px, Div, ParentElement, Render, Styled, ViewContext}; + use gpui::{div, img, px, Node, ParentComponent, Render, Styled, ViewContext}; pub struct PlayerStory; impl Render for PlayerStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx).child( diff --git a/crates/theme2/src/story.rs b/crates/theme2/src/story.rs index 8b3754b59e..00cb20df92 100644 --- a/crates/theme2/src/story.rs +++ b/crates/theme2/src/story.rs @@ -1,11 +1,11 @@ -use gpui::{div, Component, Div, ParentElement, Styled, ViewContext}; +use gpui::{div, Component, Node, ParentComponent, Styled, ViewContext}; use crate::ActiveTheme; pub struct Story {} impl Story { - pub fn container(cx: &mut ViewContext) -> Div { + pub fn container(cx: &mut ViewContext) -> Node { div() .size_full() .flex() diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index d083d8fd46..d270f2fd32 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -44,12 +44,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct AvatarStory; impl Render for AvatarStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 5787616832..eeb8ddb906 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use gpui::{div, DefiniteLength, Hsla, MouseButton, WindowContext}; +use gpui::{div, DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext}; use crate::{ h_stack, prelude::*, Icon, IconButton, IconColor, IconElement, Label, LabelColor, @@ -236,13 +236,13 @@ pub use stories::*; mod stories { use super::*; use crate::{h_stack, v_stack, LabelColor, Story}; - use gpui::{rems, Div, Render}; + use gpui::{rems, Node, Render}; use strum::IntoEnumIterator; pub struct ButtonStory; impl Render for ButtonStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let states = InteractionState::iter(); diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs index 20dad74712..c2a0cfdc2f 100644 --- a/crates/ui2/src/components/checkbox.rs +++ b/crates/ui2/src/components/checkbox.rs @@ -1,9 +1,5 @@ +use gpui::{div, prelude::*, Component, ElementId, Styled, ViewContext}; use std::sync::Arc; - -use gpui::{ - div, Component, ElementId, ParentElement, StatefulInteractive, StatelessInteractive, Styled, - ViewContext, -}; use theme2::ActiveTheme; use crate::{Icon, IconColor, IconElement, Selection}; @@ -175,12 +171,12 @@ pub use stories::*; mod stories { use super::*; use crate::{h_stack, Story}; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct CheckboxStory; impl Render for CheckboxStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 117be12779..a33f10c296 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -65,12 +65,12 @@ pub use stories::*; mod stories { use super::*; use crate::story::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct ContextMenuStory; impl Render for ContextMenuStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/details.rs b/crates/ui2/src/components/details.rs index f138290f17..44c433179a 100644 --- a/crates/ui2/src/components/details.rs +++ b/crates/ui2/src/components/details.rs @@ -47,12 +47,12 @@ pub use stories::*; mod stories { use super::*; use crate::{Button, Story}; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct DetailsStory; impl Render for DetailsStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/elevated_surface.rs b/crates/ui2/src/components/elevated_surface.rs index 7a6f11978e..3a1699a64a 100644 --- a/crates/ui2/src/components/elevated_surface.rs +++ b/crates/ui2/src/components/elevated_surface.rs @@ -1,11 +1,11 @@ -use gpui::Div; +use gpui::Node; use crate::{prelude::*, v_stack}; /// Create an elevated surface. /// /// Must be used inside of a relative parent element -pub fn elevated_surface(level: ElevationIndex, cx: &mut ViewContext) -> Div { +pub fn elevated_surface(level: ElevationIndex, cx: &mut ViewContext) -> Node { let colors = cx.theme().colors(); // let shadow = BoxShadow { @@ -23,6 +23,6 @@ pub fn elevated_surface(level: ElevationIndex, cx: &mut ViewContext< .shadow(level.shadow()) } -pub fn modal(cx: &mut ViewContext) -> Div { +pub fn modal(cx: &mut ViewContext) -> Node { elevated_surface(ElevationIndex::ModalSurface, cx) } diff --git a/crates/ui2/src/components/facepile.rs b/crates/ui2/src/components/facepile.rs index efac4925f8..43fd233458 100644 --- a/crates/ui2/src/components/facepile.rs +++ b/crates/ui2/src/components/facepile.rs @@ -33,12 +33,12 @@ pub use stories::*; mod stories { use super::*; use crate::{static_players, Story}; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct FacepileStory; impl Render for FacepileStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let players = static_players(); diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 907f3f9187..ce980a879b 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -204,7 +204,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Div, Render}; + use gpui::{Node, Render}; use strum::IntoEnumIterator; use crate::Story; @@ -214,7 +214,7 @@ mod stories { pub struct IconStory; impl Render for IconStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let icons = Icon::iter(); diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 91653ea8cd..86803c584c 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,9 +1,7 @@ -use std::sync::Arc; - -use gpui::{rems, MouseButton}; - use crate::{h_stack, prelude::*}; use crate::{ClickHandler, Icon, IconColor, IconElement}; +use gpui::{prelude::*, rems, MouseButton}; +use std::sync::Arc; struct IconButtonHandlers { click: Option>, diff --git a/crates/ui2/src/components/input.rs b/crates/ui2/src/components/input.rs index 1a44827fe8..d873d6a5cb 100644 --- a/crates/ui2/src/components/input.rs +++ b/crates/ui2/src/components/input.rs @@ -1,6 +1,5 @@ -use crate::prelude::*; -use crate::Label; -use crate::LabelColor; +use crate::{prelude::*, Label, LabelColor}; +use gpui::prelude::*; #[derive(Default, PartialEq)] pub enum InputVariant { @@ -111,12 +110,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct InputStory; impl Render for InputStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index bd02e694ed..86e876d245 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -158,13 +158,13 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; use itertools::Itertools; pub struct KeybindingStory; impl Render for KeybindingStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let all_modifier_permutations = ModifierKey::iter().permutations(2); diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 827ba87918..497ebe6773 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -196,12 +196,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct LabelStory; impl Render for LabelStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/modal.rs b/crates/ui2/src/components/modal.rs index 75528b5c34..c3d71a78d8 100644 --- a/crates/ui2/src/components/modal.rs +++ b/crates/ui2/src/components/modal.rs @@ -74,7 +74,7 @@ impl Modal { } } -impl ParentElement for Modal { +impl ParentComponent for Modal { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 7f736433fc..4a753215f0 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -1,5 +1,5 @@ -use crate::prelude::*; -use crate::{h_stack, v_stack, Keybinding, Label, LabelColor}; +use crate::{h_stack, prelude::*, v_stack, Keybinding, Label, LabelColor}; +use gpui::prelude::*; #[derive(Component)] pub struct Palette { @@ -159,7 +159,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Div, Render}; + use gpui::{Node, Render}; use crate::{ModifierKeys, Story}; @@ -168,7 +168,7 @@ mod stories { pub struct PaletteStory; impl Render for PaletteStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { { diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index 1762003a2c..2b317b0bc1 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -1,4 +1,4 @@ -use gpui::{AbsoluteLength, AnyElement}; +use gpui::{prelude::*, AbsoluteLength, AnyElement}; use smallvec::SmallVec; use crate::prelude::*; @@ -113,7 +113,7 @@ impl Panel { } } -impl ParentElement for Panel { +impl ParentComponent for Panel { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } @@ -126,12 +126,12 @@ pub use stories::*; mod stories { use super::*; use crate::{Label, Story}; - use gpui::{Div, Render}; + use gpui::{InteractiveComponent, Node, Render}; pub struct PanelStory; impl Render for PanelStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/stack.rs b/crates/ui2/src/components/stack.rs index d3d7a75aa7..d705863ad4 100644 --- a/crates/ui2/src/components/stack.rs +++ b/crates/ui2/src/components/stack.rs @@ -1,17 +1,17 @@ -use gpui::{div, Div}; +use gpui::{div, Node}; use crate::StyledExt; /// Horizontally stacks elements. /// /// Sets `flex()`, `flex_row()`, `items_center()` -pub fn h_stack() -> Div { +pub fn h_stack() -> Node { div().h_flex() } /// Vertically stacks elements. /// /// Sets `flex()`, `flex_col()` -pub fn v_stack() -> Div { +pub fn v_stack() -> Node { div().v_flex() } diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index e936dc924a..fe993555b9 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use crate::{Icon, IconColor, IconElement, Label, LabelColor}; -use gpui::{red, Div, ElementId, Render, View, VisualContext}; +use gpui::{prelude::*, red, ElementId, Node, Render, View}; #[derive(Component, Clone)] pub struct Tab { @@ -21,7 +21,7 @@ struct TabDragState { } impl Render for TabDragState { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div().w_8().h_4().bg(red()) @@ -178,7 +178,7 @@ mod stories { pub struct TabStory; impl Render for TabStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let git_statuses = GitStatus::iter(); diff --git a/crates/ui2/src/components/toast.rs b/crates/ui2/src/components/toast.rs index 4164be2c3e..2f07bbc3e0 100644 --- a/crates/ui2/src/components/toast.rs +++ b/crates/ui2/src/components/toast.rs @@ -1,7 +1,6 @@ -use gpui::AnyElement; -use smallvec::SmallVec; - use crate::prelude::*; +use gpui::{prelude::*, AnyElement}; +use smallvec::SmallVec; #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] pub enum ToastOrigin { @@ -59,7 +58,7 @@ impl Toast { } } -impl ParentElement for Toast { +impl ParentComponent for Toast { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } @@ -70,7 +69,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Div, Render}; + use gpui::{Node, Render}; use crate::{Label, Story}; @@ -79,7 +78,7 @@ mod stories { pub struct ToastStory; impl Render for ToastStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/toggle.rs b/crates/ui2/src/components/toggle.rs index 368c95662f..f34f08e09e 100644 --- a/crates/ui2/src/components/toggle.rs +++ b/crates/ui2/src/components/toggle.rs @@ -1,4 +1,4 @@ -use gpui::{div, Component, ParentElement}; +use gpui::{div, Component, ParentComponent}; use crate::{Icon, IconColor, IconElement, IconSize}; diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 87860ce943..6c5fa7bc2b 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,4 +1,4 @@ -use gpui::{div, Div, ParentElement, Render, SharedString, Styled, ViewContext}; +use gpui::{div, Node, ParentComponent, Render, SharedString, Styled, ViewContext}; use theme2::ActiveTheme; #[derive(Clone, Debug)] @@ -13,7 +13,7 @@ impl TextTooltip { } impl Render for TextTooltip { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let theme = cx.theme(); diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 545f437a9b..f37b6123e3 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -1,8 +1,8 @@ use gpui::rems; use gpui::Rems; pub use gpui::{ - div, Component, Element, ElementId, ParentElement, SharedString, StatefulInteractive, - StatelessInteractive, Styled, ViewContext, WindowContext, + div, Component, Element, ElementId, InteractiveComponent, ParentComponent, SharedString, Styled, + ViewContext, WindowContext, }; pub use crate::elevation::*; diff --git a/crates/ui2/src/story.rs b/crates/ui2/src/story.rs index 94e38267f4..cf5737a245 100644 --- a/crates/ui2/src/story.rs +++ b/crates/ui2/src/story.rs @@ -1,11 +1,11 @@ -use gpui::Div; +use gpui::Node; use crate::prelude::*; pub struct Story {} impl Story { - pub fn container(cx: &mut ViewContext) -> Div { + pub fn container(cx: &mut ViewContext) -> Node { div() .size_full() .flex() diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index 0407d98f86..d9911e6833 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -1,4 +1,4 @@ -use gpui::{Div, ElementInteractivity, KeyDispatch, Styled, UniformList, ViewContext}; +use gpui::{Styled, ViewContext}; use theme2::ActiveTheme; use crate::{ElevationIndex, UITextSize}; @@ -93,11 +93,4 @@ pub trait StyledExt: Styled + Sized { } } -impl StyledExt for Div -where - I: ElementInteractivity, - F: KeyDispatch, -{ -} - -impl StyledExt for UniformList {} +impl StyledExt for E {} diff --git a/crates/ui2/src/to_extract/assistant_panel.rs b/crates/ui2/src/to_extract/assistant_panel.rs index 8a35757f5c..59d9059122 100644 --- a/crates/ui2/src/to_extract/assistant_panel.rs +++ b/crates/ui2/src/to_extract/assistant_panel.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use crate::{Icon, IconButton, Label, Panel, PanelSide}; -use gpui::{rems, AbsoluteLength}; +use gpui::{prelude::*, rems, AbsoluteLength}; #[derive(Component)] pub struct AssistantPanel { @@ -77,11 +77,11 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct AssistantPanelStory; impl Render for AssistantPanelStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/breadcrumb.rs b/crates/ui2/src/to_extract/breadcrumb.rs index 782f772fa1..c113c64168 100644 --- a/crates/ui2/src/to_extract/breadcrumb.rs +++ b/crates/ui2/src/to_extract/breadcrumb.rs @@ -1,9 +1,7 @@ +use crate::{h_stack, prelude::*, HighlightedText}; +use gpui::{prelude::*, Node}; use std::path::PathBuf; -use crate::prelude::*; -use crate::{h_stack, HighlightedText}; -use gpui::Div; - #[derive(Clone)] pub struct Symbol(pub Vec); @@ -18,7 +16,7 @@ impl Breadcrumb { Self { path, symbols } } - fn render_separator(&self, cx: &WindowContext) -> Div { + fn render_separator(&self, cx: &WindowContext) -> Node { div() .child(" › ") .text_color(cx.theme().colors().text_muted) @@ -79,7 +77,7 @@ mod stories { pub struct BreadcrumbStory; impl Render for BreadcrumbStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/buffer.rs b/crates/ui2/src/to_extract/buffer.rs index aa4bebc9d5..b04f2221d3 100644 --- a/crates/ui2/src/to_extract/buffer.rs +++ b/crates/ui2/src/to_extract/buffer.rs @@ -235,12 +235,12 @@ mod stories { empty_buffer_example, hello_world_rust_buffer_example, hello_world_rust_buffer_with_status_example, Story, }; - use gpui::{rems, Div, Render}; + use gpui::{rems, Node, Render}; pub struct BufferStory; impl Render for BufferStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/buffer_search.rs b/crates/ui2/src/to_extract/buffer_search.rs index 9993cd3612..0e6fffa5a7 100644 --- a/crates/ui2/src/to_extract/buffer_search.rs +++ b/crates/ui2/src/to_extract/buffer_search.rs @@ -1,4 +1,4 @@ -use gpui::{Div, Render, View, VisualContext}; +use gpui::{Node, Render, View, VisualContext}; use crate::prelude::*; use crate::{h_stack, Icon, IconButton, IconColor, Input}; @@ -27,9 +27,9 @@ impl BufferSearch { } impl Render for BufferSearch { - type Element = Div; + type Element = Node; - fn render(&mut self, cx: &mut ViewContext) -> Div { + fn render(&mut self, cx: &mut ViewContext) -> Node { h_stack() .bg(cx.theme().colors().toolbar_background) .p_2() diff --git a/crates/ui2/src/to_extract/chat_panel.rs b/crates/ui2/src/to_extract/chat_panel.rs index 538b5dfceb..13f35468ff 100644 --- a/crates/ui2/src/to_extract/chat_panel.rs +++ b/crates/ui2/src/to_extract/chat_panel.rs @@ -1,7 +1,6 @@ +use crate::{prelude::*, Icon, IconButton, Input, Label, LabelColor}; use chrono::NaiveDateTime; - -use crate::prelude::*; -use crate::{Icon, IconButton, Input, Label, LabelColor}; +use gpui::prelude::*; #[derive(Component)] pub struct ChatPanel { @@ -108,7 +107,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { use chrono::DateTime; - use gpui::{Div, Render}; + use gpui::{Node, Render}; use crate::{Panel, Story}; @@ -117,7 +116,7 @@ mod stories { pub struct ChatPanelStory; impl Render for ChatPanelStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/collab_panel.rs b/crates/ui2/src/to_extract/collab_panel.rs index d56166ad2e..d2ac353e05 100644 --- a/crates/ui2/src/to_extract/collab_panel.rs +++ b/crates/ui2/src/to_extract/collab_panel.rs @@ -1,7 +1,8 @@ -use crate::{prelude::*, Toggle}; use crate::{ - static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, ListHeader, + prelude::*, static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, + List, ListHeader, Toggle, }; +use gpui::prelude::*; #[derive(Component)] pub struct CollabPanel { @@ -92,12 +93,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct CollabPanelStory; impl Render for CollabPanelStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/command_palette.rs b/crates/ui2/src/to_extract/command_palette.rs index 8a9461c796..8a9c61490c 100644 --- a/crates/ui2/src/to_extract/command_palette.rs +++ b/crates/ui2/src/to_extract/command_palette.rs @@ -27,7 +27,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Div, Render}; + use gpui::{Node, Render}; use crate::Story; @@ -36,7 +36,7 @@ mod stories { pub struct CommandPaletteStory; impl Render for CommandPaletteStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/copilot.rs b/crates/ui2/src/to_extract/copilot.rs index 8750ab3c51..2a2c2c4a27 100644 --- a/crates/ui2/src/to_extract/copilot.rs +++ b/crates/ui2/src/to_extract/copilot.rs @@ -25,7 +25,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Div, Render}; + use gpui::{Node, Render}; use crate::Story; @@ -34,7 +34,7 @@ mod stories { pub struct CopilotModalStory; impl Render for CopilotModalStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/editor_pane.rs b/crates/ui2/src/to_extract/editor_pane.rs index fd21e81242..4546c24794 100644 --- a/crates/ui2/src/to_extract/editor_pane.rs +++ b/crates/ui2/src/to_extract/editor_pane.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use gpui::{Div, Render, View, VisualContext}; +use gpui::{Node, Render, View, VisualContext}; use crate::prelude::*; use crate::{ @@ -48,9 +48,9 @@ impl EditorPane { } impl Render for EditorPane { - type Element = Div; + type Element = Node; - fn render(&mut self, cx: &mut ViewContext) -> Div { + fn render(&mut self, cx: &mut ViewContext) -> Node { v_stack() .w_full() .h_full() diff --git a/crates/ui2/src/to_extract/language_selector.rs b/crates/ui2/src/to_extract/language_selector.rs index 694ca78e9c..46a10d5c88 100644 --- a/crates/ui2/src/to_extract/language_selector.rs +++ b/crates/ui2/src/to_extract/language_selector.rs @@ -40,12 +40,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct LanguageSelectorStory; impl Render for LanguageSelectorStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/multi_buffer.rs b/crates/ui2/src/to_extract/multi_buffer.rs index 78a22d51d0..0649bf1290 100644 --- a/crates/ui2/src/to_extract/multi_buffer.rs +++ b/crates/ui2/src/to_extract/multi_buffer.rs @@ -40,12 +40,12 @@ pub use stories::*; mod stories { use super::*; use crate::{hello_world_rust_buffer_example, Story}; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct MultiBufferStory; impl Render for MultiBufferStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/notifications_panel.rs b/crates/ui2/src/to_extract/notifications_panel.rs index b2cc4a7846..0a85a15f34 100644 --- a/crates/ui2/src/to_extract/notifications_panel.rs +++ b/crates/ui2/src/to_extract/notifications_panel.rs @@ -1,10 +1,9 @@ -use crate::utils::naive_format_distance_from_now; use crate::{ - h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, ButtonOrIconButton, - Icon, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator, - PublicPlayer, UnreadIndicator, + h_stack, prelude::*, static_new_notification_items_2, utils::naive_format_distance_from_now, + v_stack, Avatar, ButtonOrIconButton, ClickHandler, Icon, IconElement, Label, LabelColor, + LineHeightStyle, ListHeader, ListHeaderMeta, ListSeparator, PublicPlayer, UnreadIndicator, }; -use crate::{ClickHandler, ListHeader}; +use gpui::prelude::*; #[derive(Component)] pub struct NotificationsPanel { @@ -353,12 +352,12 @@ pub use stories::*; mod stories { use super::*; use crate::{Panel, Story}; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct NotificationsPanelStory; impl Render for NotificationsPanelStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/panes.rs b/crates/ui2/src/to_extract/panes.rs index b57b77d5ee..288419d8bf 100644 --- a/crates/ui2/src/to_extract/panes.rs +++ b/crates/ui2/src/to_extract/panes.rs @@ -59,7 +59,7 @@ impl Pane { } } -impl ParentElement for Pane { +impl ParentComponent for Pane { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } diff --git a/crates/ui2/src/to_extract/project_panel.rs b/crates/ui2/src/to_extract/project_panel.rs index d4f5c72426..c55056a7a9 100644 --- a/crates/ui2/src/to_extract/project_panel.rs +++ b/crates/ui2/src/to_extract/project_panel.rs @@ -1,7 +1,8 @@ -use crate::prelude::*; use crate::{ - static_project_panel_project_items, static_project_panel_single_items, Input, List, ListHeader, + prelude::*, static_project_panel_project_items, static_project_panel_single_items, Input, List, + ListHeader, }; +use gpui::prelude::*; #[derive(Component)] pub struct ProjectPanel { @@ -54,12 +55,12 @@ pub use stories::*; mod stories { use super::*; use crate::{Panel, Story}; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct ProjectPanelStory; impl Render for ProjectPanelStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/recent_projects.rs b/crates/ui2/src/to_extract/recent_projects.rs index 3d4f551490..83b15a3128 100644 --- a/crates/ui2/src/to_extract/recent_projects.rs +++ b/crates/ui2/src/to_extract/recent_projects.rs @@ -36,12 +36,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct RecentProjectsStory; impl Render for RecentProjectsStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/tab_bar.rs b/crates/ui2/src/to_extract/tab_bar.rs index aff095c639..e8de2e9e58 100644 --- a/crates/ui2/src/to_extract/tab_bar.rs +++ b/crates/ui2/src/to_extract/tab_bar.rs @@ -1,5 +1,5 @@ -use crate::prelude::*; -use crate::{Icon, IconButton, Tab}; +use crate::{prelude::*, Icon, IconButton, Tab}; +use gpui::prelude::*; #[derive(Component)] pub struct TabBar { @@ -100,12 +100,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct TabBarStory; impl Render for TabBarStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/terminal.rs b/crates/ui2/src/to_extract/terminal.rs index 6c36f35152..5bcbca4fde 100644 --- a/crates/ui2/src/to_extract/terminal.rs +++ b/crates/ui2/src/to_extract/terminal.rs @@ -83,11 +83,11 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{Node, Render}; pub struct TerminalStory; impl Render for TerminalStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/theme_selector.rs b/crates/ui2/src/to_extract/theme_selector.rs index 7f911b50bf..7dd169a2f7 100644 --- a/crates/ui2/src/to_extract/theme_selector.rs +++ b/crates/ui2/src/to_extract/theme_selector.rs @@ -39,7 +39,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Div, Render}; + use gpui::{Node, Render}; use crate::Story; @@ -48,7 +48,7 @@ mod stories { pub struct ThemeSelectorStory; impl Render for ThemeSelectorStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/title_bar.rs b/crates/ui2/src/to_extract/title_bar.rs index 87d7dd4146..d805992023 100644 --- a/crates/ui2/src/to_extract/title_bar.rs +++ b/crates/ui2/src/to_extract/title_bar.rs @@ -1,7 +1,7 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; -use gpui::{Div, Render, View, VisualContext}; +use gpui::{Node, Render, View, VisualContext}; use crate::prelude::*; use crate::settings::user_settings; @@ -86,9 +86,9 @@ impl TitleBar { } impl Render for TitleBar { - type Element = Div; + type Element = Node; - fn render(&mut self, cx: &mut ViewContext) -> Div { + fn render(&mut self, cx: &mut ViewContext) -> Node { let settings = user_settings(cx); // let has_focus = cx.window_is_active(); @@ -202,9 +202,9 @@ mod stories { } impl Render for TitleBarStory { - type Element = Div; + type Element = Node; - fn render(&mut self, cx: &mut ViewContext) -> Div { + fn render(&mut self, cx: &mut ViewContext) -> Node { Story::container(cx) .child(Story::title_for::<_, TitleBar>(cx)) .child(Story::label(cx, "Default")) diff --git a/crates/ui2/src/to_extract/toolbar.rs b/crates/ui2/src/to_extract/toolbar.rs index 81918f34a7..1c1795437b 100644 --- a/crates/ui2/src/to_extract/toolbar.rs +++ b/crates/ui2/src/to_extract/toolbar.rs @@ -73,7 +73,7 @@ mod stories { use std::path::PathBuf; use std::str::FromStr; - use gpui::{Div, Render}; + use gpui::{Node, Render}; use crate::{Breadcrumb, HighlightedText, Icon, IconButton, Story, Symbol}; @@ -82,7 +82,7 @@ mod stories { pub struct ToolbarStory; impl Render for ToolbarStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/traffic_lights.rs b/crates/ui2/src/to_extract/traffic_lights.rs index 245ff377f2..76898eb19a 100644 --- a/crates/ui2/src/to_extract/traffic_lights.rs +++ b/crates/ui2/src/to_extract/traffic_lights.rs @@ -77,7 +77,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Div, Render}; + use gpui::{Node, Render}; use crate::Story; @@ -86,7 +86,7 @@ mod stories { pub struct TrafficLightsStory; impl Render for TrafficLightsStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/workspace.rs b/crates/ui2/src/to_extract/workspace.rs index d6de8a8288..2ab6b4b3ef 100644 --- a/crates/ui2/src/to_extract/workspace.rs +++ b/crates/ui2/src/to_extract/workspace.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use chrono::DateTime; -use gpui::{px, relative, Div, Render, Size, View, VisualContext}; +use gpui::{px, relative, Node, Render, Size, View, VisualContext}; use settings2::Settings; use theme2::ThemeSettings; @@ -192,9 +192,9 @@ impl Workspace { } impl Render for Workspace { - type Element = Div; + type Element = Node; - fn render(&mut self, cx: &mut ViewContext) -> Div { + fn render(&mut self, cx: &mut ViewContext) -> Node { let root_group = PaneGroup::new_panes( vec![Pane::new( "pane-0", @@ -388,7 +388,7 @@ mod stories { } impl Render for WorkspaceStory { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div().child(self.workspace.clone()) diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index f21eb84ae2..0e507c0f7c 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,7 +1,7 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, ParentElement, Render, - Subscription, View, ViewContext, WeakView, WindowContext, + div, Action, AnyView, AppContext, Entity, EntityId, EventEmitter, Node, ParentComponent, + Render, Subscription, View, ViewContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -419,7 +419,7 @@ impl Dock { } impl Render for Dock { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { todo!() @@ -621,7 +621,7 @@ impl PanelButtons { // } impl Render for PanelButtons { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { // todo!() @@ -647,7 +647,7 @@ impl StatusItemView for PanelButtons { #[cfg(any(test, feature = "test-support"))] pub mod test { use super::*; - use gpui::{div, Div, ViewContext, WindowContext}; + use gpui::{div, Node, ViewContext, WindowContext}; pub struct TestPanel { pub position: DockPosition, @@ -672,7 +672,7 @@ pub mod test { } impl Render for TestPanel { - type Element = Div; + type Element = Node; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 09ffa6c13f..8beaaed3c3 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,6 +1,6 @@ use gpui::{ - div, px, AnyView, Div, EventEmitter, FocusHandle, ParentElement, Render, StatelessInteractive, - Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + div, prelude::*, px, AnyView, EventEmitter, FocusHandle, InteractiveComponent, Node, + ParentComponent, Render, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use ui::v_stack; @@ -76,7 +76,7 @@ impl ModalLayer { } impl Render for ModalLayer { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let Some(active_modal) = &self.active_modal else { diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs index 7277cc6fc4..8673645b1e 100644 --- a/crates/workspace2/src/notifications.rs +++ b/crates/workspace2/src/notifications.rs @@ -165,7 +165,7 @@ impl Workspace { pub mod simple_message_notification { use super::{Notification, NotificationEvent}; - use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext}; + use gpui::{AnyElement, AppContext, EventEmitter, Node, Render, TextStyle, ViewContext}; use serde::Deserialize; use std::{borrow::Cow, sync::Arc}; @@ -252,7 +252,7 @@ pub mod simple_message_notification { } impl Render for MessageNotification { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { todo!() diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 2bba684d12..ff16ebdc6d 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1,5 +1,3 @@ -// mod dragged_item_receiver; - use crate::{ item::{Item, ItemHandle, ItemSettings, WeakItemHandle}, toolbar::Toolbar, @@ -9,9 +7,9 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - actions, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId, - EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, - WeakView, WindowContext, + actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, EntityId, + EventEmitter, FocusHandle, Model, Node, PromptLevel, Render, Task, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -1903,7 +1901,7 @@ impl Pane { // } impl Render for Pane { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { v_stack() @@ -2928,7 +2926,7 @@ struct DraggedTab { } impl Render for DraggedTab { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div().w_8().h_4().bg(gpui::red()) diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index fcf6ac3b61..6ac524f824 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -2,8 +2,8 @@ use std::any::TypeId; use crate::{ItemHandle, Pane}; use gpui::{ - div, AnyView, Component, Div, ParentElement, Render, Styled, Subscription, View, ViewContext, - WindowContext, + div, AnyView, Component, Node, ParentComponent, Render, Styled, Subscription, View, + ViewContext, WindowContext, }; use theme2::ActiveTheme; use util::ResultExt; @@ -34,7 +34,7 @@ pub struct StatusBar { } impl Render for StatusBar { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 1d67da06b2..dfe661d5b0 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -1,6 +1,6 @@ use crate::ItemHandle; use gpui::{ - AnyView, Div, Entity, EntityId, EventEmitter, Render, View, ViewContext, WindowContext, + AnyView, Entity, EntityId, EventEmitter, Node, Render, View, ViewContext, WindowContext, }; pub enum ToolbarItemEvent { @@ -52,7 +52,7 @@ pub struct Toolbar { } impl Render for Toolbar { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { todo!() diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 14a7685a9b..df2b1d8f30 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -36,12 +36,11 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, - AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, - StatefulInteractive, StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, - View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, - WindowOptions, + actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView, + AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Entity, EntityId, + EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, Node, + ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, + WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -443,7 +442,6 @@ struct Follower { impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { - use gpui::Context; use node_runtime::FakeNodeRuntime; use settings2::SettingsStore; @@ -531,13 +529,7 @@ pub enum Event { pub struct Workspace { weak_self: WeakView, focus_handle: FocusHandle, - workspace_actions: Vec< - Box< - dyn Fn( - Div>, - ) -> Div>, - >, - >, + workspace_actions: Vec) -> Node>>, zoomed: Option, zoomed_position: Option, center: PaneGroup, @@ -3450,7 +3442,6 @@ impl Workspace { #[cfg(any(test, feature = "test-support"))] pub fn test_new(project: Model, cx: &mut ViewContext) -> Self { - use gpui::Context; use node_runtime::FakeNodeRuntime; let client = project.read(cx).client(); @@ -3512,10 +3503,7 @@ impl Workspace { })); } - fn add_workspace_actions_listeners( - &self, - mut div: Div>, - ) -> Div> { + fn add_workspace_actions_listeners(&self, mut div: Node) -> Node { for action in self.workspace_actions.iter() { div = (action)(div) } @@ -3740,14 +3728,14 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA impl EventEmitter for Workspace {} impl Render for Workspace { - type Element = Div; + type Element = Node; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); context.add("Workspace"); self.add_workspace_actions_listeners(div()) - .context(context) + .key_context(context) .relative() .size_full() .flex() From 80014a28ea1f675fb5c5dc4b8f96221d37abf338 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 01:23:09 -0700 Subject: [PATCH 077/126] No compile errors or warnings --- crates/gpui2/src/elements/img.rs | 8 +- crates/gpui2/src/elements/svg.rs | 6 +- crates/gpui2/src/interactive.rs | 47 +------- crates/gpui2/src/key_dispatch.rs | 153 +-------------------------- crates/gpui2/src/styled.rs | 3 +- crates/workspace2/src/modal_layer.rs | 4 +- 6 files changed, 15 insertions(+), 206 deletions(-) diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index bfa3c6cfae..c5c5fb628e 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -50,7 +50,7 @@ impl Element for Img { fn initialize( &mut self, - view_state: &mut V, + _view_state: &mut V, element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { @@ -59,7 +59,7 @@ impl Element for Img { fn layout( &mut self, - view_state: &mut V, + _view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { @@ -71,7 +71,7 @@ impl Element for Img { fn paint( &mut self, bounds: Bounds, - view: &mut V, + _view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { @@ -80,7 +80,7 @@ impl Element for Img { bounds.size, element_state, cx, - |style, scroll_offset, cx| { + |style, _scroll_offset, cx| { let corner_radii = style.corner_radii; if let Some(uri) = self.uri.clone() { diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index d0b321c0e2..4b441ad425 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -39,7 +39,7 @@ impl Element for Svg { fn initialize( &mut self, - view_state: &mut V, + _view_state: &mut V, element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { @@ -48,7 +48,7 @@ impl Element for Svg { fn layout( &mut self, - view_state: &mut V, + _view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { @@ -60,7 +60,7 @@ impl Element for Svg { fn paint( &mut self, bounds: Bounds, - view: &mut V, + _view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) where diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 702a7ca5db..cb8be7c296 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,16 +1,9 @@ use crate::{ - div, point, px, AnyDrag, AnyTooltip, AnyView, AppContext, Bounds, Component, DispatchPhase, - FocusHandle, Keystroke, Modifiers, Node, Pixels, Point, Render, SharedString, StyleRefinement, - Task, ViewContext, + div, point, Component, FocusHandle, Keystroke, Modifiers, Node, Pixels, Point, Render, + ViewContext, }; use smallvec::SmallVec; -use std::{ - any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf, sync::Arc, time::Duration, -}; - -const DRAG_THRESHOLD: f64 = 2.; -const TOOLTIP_DELAY: Duration = Duration::from_millis(500); -const TOOLTIP_OFFSET: Point = Point::new(px(10.0), px(8.0)); +use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct KeyDownEvent { @@ -290,40 +283,6 @@ pub struct FocusEvent { pub focused: Option, } -pub type MouseDownListener = Box< - dyn Fn(&mut V, &MouseDownEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, ->; -pub type MouseUpListener = Box< - dyn Fn(&mut V, &MouseUpEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, ->; - -pub type MouseMoveListener = Box< - dyn Fn(&mut V, &MouseMoveEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, ->; - -pub type ScrollWheelListener = Box< - dyn Fn(&mut V, &ScrollWheelEvent, &Bounds, DispatchPhase, &mut ViewContext) - + 'static, ->; - -pub type ClickListener = Box) + 'static>; - -pub(crate) type DragListener = - Box, &mut ViewContext) -> AnyDrag + 'static>; - -pub(crate) type HoverListener = Box) + 'static>; - -pub(crate) type TooltipBuilder = Arc) -> AnyView + 'static>; - -pub(crate) type KeyDownListener = - Box) + 'static>; - -pub(crate) type KeyUpListener = - Box) + 'static>; - -pub type ActionListener = - Box) + 'static>; - #[cfg(test)] mod test { use crate::{ diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index afe0b0f3e2..b06acae43d 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -1,11 +1,9 @@ use crate::{ - build_action_from_type, Action, Bounds, DispatchPhase, FocusEvent, FocusHandle, FocusId, - KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, Pixels, Style, - StyleRefinement, ViewContext, WindowContext, + build_action_from_type, Action, DispatchPhase, FocusId, KeyContext, KeyMatch, Keymap, + Keystroke, KeystrokeMatcher, WindowContext, }; use collections::HashMap; use parking_lot::Mutex; -use refineable::Refineable; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -14,10 +12,6 @@ use std::{ }; use util::ResultExt; -pub type FocusListeners = SmallVec<[FocusListener; 2]>; -pub type FocusListener = - Box) + 'static>; - #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct DispatchNodeId(usize); @@ -199,146 +193,3 @@ impl DispatchTree { *self.node_stack.last().unwrap() } } - -pub trait KeyDispatch: 'static { - fn as_focusable(&self) -> Option<&FocusableKeyDispatch>; - fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch>; - fn key_context(&self) -> &KeyContext; - fn key_context_mut(&mut self) -> &mut KeyContext; - - fn initialize( - &mut self, - focus_handle: Option, - cx: &mut ViewContext, - f: impl FnOnce(Option, &mut ViewContext) -> R, - ) -> R { - let focus_handle = if let Some(focusable) = self.as_focusable_mut() { - let focus_handle = focusable - .focus_handle - .get_or_insert_with(|| focus_handle.unwrap_or_else(|| cx.focus_handle())) - .clone(); - for listener in focusable.focus_listeners.drain(..) { - let focus_handle = focus_handle.clone(); - cx.on_focus_changed(move |view, event, cx| { - listener(view, &focus_handle, event, cx) - }); - } - Some(focus_handle) - } else { - None - }; - - cx.with_key_dispatch(self.key_context().clone(), focus_handle, f) - } - - fn refine_style(&self, style: &mut Style, cx: &WindowContext) { - if let Some(focusable) = self.as_focusable() { - let focus_handle = focusable - .focus_handle - .as_ref() - .expect("must call initialize before refine_style"); - if focus_handle.contains_focused(cx) { - style.refine(&focusable.focus_in_style); - } - - if focus_handle.within_focused(cx) { - style.refine(&focusable.in_focus_style); - } - - if focus_handle.is_focused(cx) { - style.refine(&focusable.focus_style); - } - } - } - - fn paint(&self, bounds: Bounds, cx: &mut WindowContext) { - if let Some(focusable) = self.as_focusable() { - let focus_handle = focusable - .focus_handle - .clone() - .expect("must call initialize before paint"); - cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - if !cx.default_prevented() { - cx.focus(&focus_handle); - cx.prevent_default(); - } - } - }) - } - } -} - -pub struct FocusableKeyDispatch { - pub non_focusable: NonFocusableKeyDispatch, - pub focus_handle: Option, - pub focus_listeners: FocusListeners, - pub focus_style: StyleRefinement, - pub focus_in_style: StyleRefinement, - pub in_focus_style: StyleRefinement, -} - -impl FocusableKeyDispatch { - pub fn new(non_focusable: NonFocusableKeyDispatch) -> Self { - Self { - non_focusable, - focus_handle: None, - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } - } - - pub fn tracked(non_focusable: NonFocusableKeyDispatch, handle: &FocusHandle) -> Self { - Self { - non_focusable, - focus_handle: Some(handle.clone()), - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } - } -} - -impl KeyDispatch for FocusableKeyDispatch { - fn as_focusable(&self) -> Option<&FocusableKeyDispatch> { - Some(self) - } - - fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch> { - Some(self) - } - - fn key_context(&self) -> &KeyContext { - &self.non_focusable.key_context - } - - fn key_context_mut(&mut self) -> &mut KeyContext { - &mut self.non_focusable.key_context - } -} - -#[derive(Default)] -pub struct NonFocusableKeyDispatch { - pub(crate) key_context: KeyContext, -} - -impl KeyDispatch for NonFocusableKeyDispatch { - fn as_focusable(&self) -> Option<&FocusableKeyDispatch> { - None - } - - fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch> { - None - } - - fn key_context(&self) -> &KeyContext { - &self.key_context - } - - fn key_context_mut(&mut self) -> &mut KeyContext { - &mut self.key_context - } -} diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index 71e38efd9b..beaf664dd8 100644 --- a/crates/gpui2/src/styled.rs +++ b/crates/gpui2/src/styled.rs @@ -1,10 +1,9 @@ use crate::{ self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, - SharedString, Style, StyleRefinement, Visibility, + SharedString, StyleRefinement, Visibility, }; use crate::{BoxShadow, TextStyleRefinement}; -use refineable::Refineable; use smallvec::{smallvec, SmallVec}; use taffy::style::Overflow; diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 8beaaed3c3..6cc08d56a6 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,6 +1,6 @@ use gpui::{ - div, prelude::*, px, AnyView, EventEmitter, FocusHandle, InteractiveComponent, Node, - ParentComponent, Render, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + div, prelude::*, px, AnyView, EventEmitter, FocusHandle, Node, Render, Subscription, View, + ViewContext, WindowContext, }; use ui::v_stack; From a5306c23122de470e7483ddc9b20ebc2c7b0f7f3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 01:25:10 -0700 Subject: [PATCH 078/126] Remove div module --- crates/gpui2/src/elements/div.rs | 334 ------------------------------- 1 file changed, 334 deletions(-) delete mode 100644 crates/gpui2/src/elements/div.rs diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs deleted file mode 100644 index 537c146339..0000000000 --- a/crates/gpui2/src/elements/div.rs +++ /dev/null @@ -1,334 +0,0 @@ -use std::fmt::Debug; - -use crate::{ - point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, ElementInteractivity, - FocusHandle, FocusListeners, Focusable, FocusableKeyDispatch, GroupBounds, - InteractiveElementState, KeyContext, KeyDispatch, LayoutId, NonFocusableKeyDispatch, Overflow, - ParentElement, Pixels, Point, SharedString, StatefulInteractive, StatefulInteractivity, - StatelessInteractive, StatelessInteractivity, Style, StyleRefinement, Styled, ViewContext, - Visibility, -}; -use refineable::Refineable; -use smallvec::SmallVec; -use util::ResultExt; - -pub struct Div< - V: 'static, - I: ElementInteractivity = StatelessInteractivity, - K: KeyDispatch = NonFocusableKeyDispatch, -> { - interactivity: I, - key_dispatch: K, - children: SmallVec<[AnyElement; 2]>, - group: Option, - base_style: StyleRefinement, -} - -pub fn div() -> Div, NonFocusableKeyDispatch> { - Div { - interactivity: StatelessInteractivity::default(), - key_dispatch: NonFocusableKeyDispatch::default(), - children: SmallVec::new(), - group: None, - base_style: StyleRefinement::default(), - } -} - -impl Div, F> -where - V: 'static, - F: KeyDispatch, -{ - pub fn id(self, id: impl Into) -> Div, F> { - Div { - interactivity: StatefulInteractivity::new(id.into(), self.interactivity), - key_dispatch: self.key_dispatch, - children: self.children, - group: self.group, - base_style: self.base_style, - } - } -} - -impl Div -where - I: ElementInteractivity, - F: KeyDispatch, -{ - pub fn context(mut self, context: C) -> Self - where - Self: Sized, - C: TryInto, - C::Error: Debug, - { - if let Some(context) = context.try_into().log_err() { - *self.key_dispatch.key_context_mut() = context; - } - self - } - - pub fn compute_style( - &self, - bounds: Bounds, - element_state: &DivState, - cx: &mut ViewContext, - ) -> Style { - let mut computed_style = Style::default(); - computed_style.refine(&self.base_style); - self.key_dispatch.refine_style(&mut computed_style, cx); - self.interactivity.refine_style( - &mut computed_style, - bounds, - &element_state.interactive, - cx, - ); - computed_style - } -} - -impl Div, NonFocusableKeyDispatch> { - pub fn focusable(self) -> Div, FocusableKeyDispatch> { - Div { - interactivity: self.interactivity, - key_dispatch: FocusableKeyDispatch::new(self.key_dispatch), - children: self.children, - group: self.group, - base_style: self.base_style, - } - } - - pub fn track_focus( - self, - handle: &FocusHandle, - ) -> Div, FocusableKeyDispatch> { - Div { - interactivity: self.interactivity, - key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle), - children: self.children, - group: self.group, - base_style: self.base_style, - } - } -} - -impl Div, NonFocusableKeyDispatch> { - pub fn track_focus( - self, - handle: &FocusHandle, - ) -> Div, FocusableKeyDispatch> { - Div { - interactivity: self.interactivity.into_stateful(handle), - key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle), - children: self.children, - group: self.group, - base_style: self.base_style, - } - } -} - -impl Focusable for Div> -where - V: 'static, - I: ElementInteractivity, -{ - fn focus_listeners(&mut self) -> &mut FocusListeners { - &mut self.key_dispatch.focus_listeners - } - - fn set_focus_style(&mut self, style: StyleRefinement) { - self.key_dispatch.focus_style = style; - } - - fn set_focus_in_style(&mut self, style: StyleRefinement) { - self.key_dispatch.focus_in_style = style; - } - - fn set_in_focus_style(&mut self, style: StyleRefinement) { - self.key_dispatch.in_focus_style = style; - } -} - -#[derive(Default)] -pub struct DivState { - interactive: InteractiveElementState, - focus_handle: Option, - child_layout_ids: SmallVec<[LayoutId; 4]>, -} - -impl Element for Div -where - I: ElementInteractivity, - F: KeyDispatch, -{ - type ElementState = DivState; - - fn id(&self) -> Option { - self.interactivity - .as_stateful() - .map(|identified| identified.id.clone()) - } - - fn initialize( - &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> Self::ElementState { - let mut element_state = element_state.unwrap_or_default(); - cx.with_element_id(self.id(), |cx| { - self.key_dispatch.initialize( - element_state.focus_handle.take(), - cx, - |focus_handle, cx| { - self.interactivity.initialize(cx); - element_state.focus_handle = focus_handle; - for child in &mut self.children { - child.initialize(view_state, cx); - } - }, - ); - }); - element_state - } - - fn layout( - &mut self, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { - let style = self.compute_style(Bounds::default(), element_state, cx); - style.apply_text_style(cx, |cx| { - cx.with_element_id(self.id(), |cx| { - let layout_ids = self - .children - .iter_mut() - .map(|child| child.layout(view_state, cx)) - .collect::>(); - element_state.child_layout_ids = layout_ids.clone(); - cx.request_layout(&style, layout_ids) - }) - }) - } - - fn paint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - cx.with_element_id(self.id(), |cx| { - let style = self.compute_style(bounds, element_state, cx); - if style.visibility == Visibility::Hidden { - return; - } - - if let Some(mouse_cursor) = style.mouse_cursor { - let hovered = bounds.contains_point(&cx.mouse_position()); - if hovered { - cx.set_cursor_style(mouse_cursor); - } - } - - if let Some(group) = self.group.clone() { - GroupBounds::push(group, bounds, cx); - } - - let z_index = style.z_index.unwrap_or(0); - - let mut child_min = point(Pixels::MAX, Pixels::MAX); - let mut child_max = Point::default(); - - let content_size = if element_state.child_layout_ids.is_empty() { - bounds.size - } else { - for child_layout_id in &element_state.child_layout_ids { - let child_bounds = cx.layout_bounds(*child_layout_id); - child_min = child_min.min(&child_bounds.origin); - child_max = child_max.max(&child_bounds.lower_right()); - } - (child_max - child_min).into() - }; - - cx.with_z_index(z_index, |cx| { - cx.with_z_index(0, |cx| { - style.paint(bounds, cx); - self.key_dispatch.paint(bounds, cx); - self.interactivity.handle_events( - bounds, - content_size, - style.overflow, - &mut element_state.interactive, - cx, - ); - }); - cx.with_z_index(1, |cx| { - style.apply_text_style(cx, |cx| { - style.apply_overflow(bounds, cx, |cx| { - let scroll_offset = element_state.interactive.scroll_offset(); - cx.with_element_offset(scroll_offset.unwrap_or_default(), |cx| { - for child in &mut self.children { - child.paint(view_state, cx); - } - }); - }) - }) - }); - }); - - if let Some(group) = self.group.as_ref() { - GroupBounds::pop(group, cx); - } - }) - } -} - -impl Component for Div -where - I: ElementInteractivity, - F: KeyDispatch, -{ - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -impl ParentElement for Div -where - I: ElementInteractivity, - F: KeyDispatch, -{ - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children - } -} - -impl Styled for Div -where - I: ElementInteractivity, - F: KeyDispatch, -{ - fn style(&mut self) -> &mut StyleRefinement { - &mut self.base_style - } -} - -impl StatelessInteractive for Div -where - I: ElementInteractivity, - F: KeyDispatch, -{ - fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { - self.interactivity.as_stateless_mut() - } -} - -impl StatefulInteractive for Div, F> -where - F: KeyDispatch, -{ - fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { - &mut self.interactivity - } -} From be18c47912ca6baea99d6da11aa38b4d5008f183 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 01:38:13 -0700 Subject: [PATCH 079/126] Remove unnecessary with_element_id calls --- crates/gpui2/src/elements/node.rs | 68 +++++++++++++++---------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs index 49c447254e..7e61c8fbb4 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/node.rs @@ -746,13 +746,11 @@ where f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, ) -> LayoutId { let style = self.compute_style(None, element_state, cx); - cx.with_element_id(self.element_id.clone(), |cx| { - cx.with_key_dispatch( - self.key_context.clone(), - self.tracked_focus_handle.clone(), - |_, cx| f(style, cx), - ) - }) + cx.with_key_dispatch( + self.key_context.clone(), + self.tracked_focus_handle.clone(), + |_, cx| f(style, cx), + ) } pub fn paint( @@ -1037,40 +1035,38 @@ where .as_ref() .map(|scroll_offset| *scroll_offset.lock()); - cx.with_element_id(self.element_id.clone(), |cx| { - cx.with_key_dispatch( - self.key_context.clone(), - element_state.focus_handle.clone(), - |_, cx| { - for listener in self.key_down_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { - listener(state, event, phase, cx); - }) - } + cx.with_key_dispatch( + self.key_context.clone(), + element_state.focus_handle.clone(), + |_, cx| { + for listener in self.key_down_listeners.drain(..) { + cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { + listener(state, event, phase, cx); + }) + } - for listener in self.key_up_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { - listener(state, event, phase, cx); - }) - } + for listener in self.key_up_listeners.drain(..) { + cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { + listener(state, event, phase, cx); + }) + } - for (action_type, listener) in self.action_listeners.drain(..) { - cx.on_action(action_type, listener) - } + for (action_type, listener) in self.action_listeners.drain(..) { + cx.on_action(action_type, listener) + } - if let Some(focus_handle) = element_state.focus_handle.as_ref() { - for listener in self.focus_listeners.drain(..) { - let focus_handle = focus_handle.clone(); - cx.on_focus_changed(move |view, event, cx| { - listener(view, &focus_handle, event, cx) - }); - } + if let Some(focus_handle) = element_state.focus_handle.as_ref() { + for listener in self.focus_listeners.drain(..) { + let focus_handle = focus_handle.clone(); + cx.on_focus_changed(move |view, event, cx| { + listener(view, &focus_handle, event, cx) + }); } + } - f(style, scroll_offset.unwrap_or_default(), cx) - }, - ); - }); + f(style, scroll_offset.unwrap_or_default(), cx) + }, + ); if let Some(group) = self.group.as_ref() { GroupBounds::pop(group, cx); From c6e8a097a37bef59bc5a5e5a25e6edf831907a98 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 01:41:55 -0700 Subject: [PATCH 080/126] Rename back to div --- crates/command_palette2/src/command_palette.rs | 8 ++++---- crates/go_to_line2/src/go_to_line.rs | 6 +++--- crates/gpui2/src/elements/{node.rs => div.rs} | 16 ++++++++-------- .../gpui2/src/{elements.rs => elements/mod.rs} | 6 ++---- crates/gpui2/src/interactive.rs | 8 ++++---- crates/picker2/src/picker2.rs | 4 ++-- crates/storybook2/src/stories/colors.rs | 4 ++-- crates/storybook2/src/stories/focus.rs | 4 ++-- crates/storybook2/src/stories/kitchen_sink.rs | 4 ++-- crates/storybook2/src/stories/picker.rs | 6 +++--- crates/storybook2/src/stories/scroll.rs | 4 ++-- crates/storybook2/src/stories/text.rs | 4 ++-- crates/storybook2/src/stories/z_index.rs | 6 +++--- crates/storybook2/src/storybook2.rs | 4 ++-- crates/theme2/src/players.rs | 4 ++-- crates/theme2/src/story.rs | 4 ++-- crates/ui2/src/components/avatar.rs | 4 ++-- crates/ui2/src/components/button.rs | 4 ++-- crates/ui2/src/components/checkbox.rs | 4 ++-- crates/ui2/src/components/context_menu.rs | 4 ++-- crates/ui2/src/components/details.rs | 4 ++-- crates/ui2/src/components/elevated_surface.rs | 6 +++--- crates/ui2/src/components/facepile.rs | 4 ++-- crates/ui2/src/components/icon.rs | 4 ++-- crates/ui2/src/components/input.rs | 4 ++-- crates/ui2/src/components/keybinding.rs | 4 ++-- crates/ui2/src/components/label.rs | 4 ++-- crates/ui2/src/components/palette.rs | 4 ++-- crates/ui2/src/components/panel.rs | 4 ++-- crates/ui2/src/components/stack.rs | 6 +++--- crates/ui2/src/components/tab.rs | 6 +++--- crates/ui2/src/components/toast.rs | 4 ++-- crates/ui2/src/components/tooltip.rs | 4 ++-- crates/ui2/src/story.rs | 4 ++-- crates/ui2/src/to_extract/assistant_panel.rs | 4 ++-- crates/ui2/src/to_extract/breadcrumb.rs | 6 +++--- crates/ui2/src/to_extract/buffer.rs | 4 ++-- crates/ui2/src/to_extract/buffer_search.rs | 6 +++--- crates/ui2/src/to_extract/chat_panel.rs | 4 ++-- crates/ui2/src/to_extract/collab_panel.rs | 4 ++-- crates/ui2/src/to_extract/command_palette.rs | 4 ++-- crates/ui2/src/to_extract/copilot.rs | 4 ++-- crates/ui2/src/to_extract/editor_pane.rs | 6 +++--- crates/ui2/src/to_extract/language_selector.rs | 4 ++-- crates/ui2/src/to_extract/multi_buffer.rs | 4 ++-- crates/ui2/src/to_extract/notifications_panel.rs | 4 ++-- crates/ui2/src/to_extract/project_panel.rs | 4 ++-- crates/ui2/src/to_extract/recent_projects.rs | 4 ++-- crates/ui2/src/to_extract/tab_bar.rs | 4 ++-- crates/ui2/src/to_extract/terminal.rs | 4 ++-- crates/ui2/src/to_extract/theme_selector.rs | 4 ++-- crates/ui2/src/to_extract/title_bar.rs | 10 +++++----- crates/ui2/src/to_extract/toolbar.rs | 4 ++-- crates/ui2/src/to_extract/traffic_lights.rs | 4 ++-- crates/ui2/src/to_extract/workspace.rs | 8 ++++---- crates/workspace2/src/dock.rs | 10 +++++----- crates/workspace2/src/modal_layer.rs | 4 ++-- crates/workspace2/src/notifications.rs | 4 ++-- crates/workspace2/src/pane.rs | 6 +++--- crates/workspace2/src/status_bar.rs | 4 ++-- crates/workspace2/src/toolbar.rs | 4 ++-- crates/workspace2/src/workspace2.rs | 8 ++++---- 62 files changed, 154 insertions(+), 156 deletions(-) rename crates/gpui2/src/elements/{node.rs => div.rs} (99%) rename crates/gpui2/src/{elements.rs => elements/mod.rs} (67%) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 139d8a16c7..cb0f2c20d4 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,8 +1,8 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, prelude::*, Action, AppContext, Component, EventEmitter, FocusHandle, Keystroke, - Node, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, + actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle, + Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; @@ -77,7 +77,7 @@ impl Modal for CommandPalette { } impl Render for CommandPalette { - type Element = Node; + type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { v_stack().w_96().child(self.picker.clone()) @@ -148,7 +148,7 @@ impl CommandPaletteDelegate { } impl PickerDelegate for CommandPaletteDelegate { - type ListItem = Node>; + type ListItem = Div>; fn placeholder_text(&self) -> Arc { "Execute a command...".into() diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 9f2ba6db21..abafe93cdd 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,7 +1,7 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, prelude::*, AppContext, EventEmitter, Node, ParentComponent, Render, - SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString, + Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; @@ -145,7 +145,7 @@ impl GoToLine { } impl Render for GoToLine { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { modal(cx) diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/div.rs similarity index 99% rename from crates/gpui2/src/elements/node.rs rename to crates/gpui2/src/elements/div.rs index 7e61c8fbb4..c0f916ae96 100644 --- a/crates/gpui2/src/elements/node.rs +++ b/crates/gpui2/src/elements/div.rs @@ -538,37 +538,37 @@ pub type KeyUpListener = pub type ActionListener = Box) + 'static>; -pub fn div() -> Node { - Node { +pub fn div() -> Div { + Div { interactivity: Interactivity::default(), children: SmallVec::default(), } } -pub struct Node { +pub struct Div { interactivity: Interactivity, children: SmallVec<[AnyElement; 2]>, } -impl Styled for Node { +impl Styled for Div { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveComponent for Node { +impl InteractiveComponent for Div { fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } } -impl ParentComponent for Node { +impl ParentComponent for Div { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } } -impl Element for Node { +impl Element for Div { type ElementState = NodeState; fn element_id(&self) -> Option { @@ -671,7 +671,7 @@ impl Element for Node { } } -impl Component for Node { +impl Component for Div { fn render(self) -> AnyElement { AnyElement::new(self) } diff --git a/crates/gpui2/src/elements.rs b/crates/gpui2/src/elements/mod.rs similarity index 67% rename from crates/gpui2/src/elements.rs rename to crates/gpui2/src/elements/mod.rs index 5bf9e6202b..eb061f7d34 100644 --- a/crates/gpui2/src/elements.rs +++ b/crates/gpui2/src/elements/mod.rs @@ -1,13 +1,11 @@ -// mod div; +mod div; mod img; -mod node; mod svg; mod text; mod uniform_list; -// pub use div::*; +pub use div::*; pub use img::*; -pub use node::*; pub use svg::*; pub use text::*; pub use uniform_list::*; diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index cb8be7c296..5ce78553fd 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,5 +1,5 @@ use crate::{ - div, point, Component, FocusHandle, Keystroke, Modifiers, Node, Pixels, Point, Render, + div, point, Component, FocusHandle, Keystroke, Modifiers, Div, Pixels, Point, Render, ViewContext, }; use smallvec::SmallVec; @@ -194,7 +194,7 @@ impl Deref for MouseExitEvent { pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); impl Render for ExternalPaths { - type Element = Node; + type Element = Div; fn render(&mut self, _: &mut ViewContext) -> Self::Element { div() // Intentionally left empty because the platform will render icons for the dragged files @@ -286,7 +286,7 @@ pub struct FocusEvent { #[cfg(test)] mod test { use crate::{ - self as gpui, div, FocusHandle, InteractiveComponent, KeyBinding, Keystroke, Node, + self as gpui, div, FocusHandle, InteractiveComponent, KeyBinding, Keystroke, Div, ParentComponent, Render, Stateful, TestAppContext, VisualContext, }; @@ -299,7 +299,7 @@ mod test { actions!(TestAction); impl Render for TestView { - type Element = Stateful>; + type Element = Stateful>; fn render(&mut self, _: &mut gpui::ViewContext) -> Self::Element { div().id("testview").child( diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 189c07b49a..0a2a50deb6 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,6 +1,6 @@ use editor::Editor; use gpui::{ - div, uniform_list, Component, Node, ParentComponent, Render, Styled, Task, + div, uniform_list, Component, Div, ParentComponent, Render, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; use std::{cmp, sync::Arc}; @@ -139,7 +139,7 @@ impl Picker { } impl Render for Picker { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/storybook2/src/stories/colors.rs b/crates/storybook2/src/stories/colors.rs index a5e9efc0da..4f8c54fa6f 100644 --- a/crates/storybook2/src/stories/colors.rs +++ b/crates/storybook2/src/stories/colors.rs @@ -1,12 +1,12 @@ use crate::story::Story; -use gpui::{prelude::*, px, Node, Render}; +use gpui::{prelude::*, px, Div, Render}; use theme2::{default_color_scales, ColorScaleStep}; use ui::prelude::*; pub struct ColorsStory; impl Render for ColorsStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let color_scales = default_color_scales(); diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index a6b27c460d..ff9a4da876 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -1,5 +1,5 @@ use gpui::{ - actions, div, prelude::*, FocusHandle, Focusable, KeyBinding, Node, Render, Stateful, View, + actions, div, prelude::*, FocusHandle, Focusable, KeyBinding, Div, Render, Stateful, View, WindowContext, }; use theme2::ActiveTheme; @@ -27,7 +27,7 @@ impl FocusStory { } impl Render for FocusStory { - type Element = Focusable>>; + type Element = Focusable>>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index 0a165eff74..507aa8db2d 100644 --- a/crates/storybook2/src/stories/kitchen_sink.rs +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -1,5 +1,5 @@ use crate::{story::Story, story_selector::ComponentStory}; -use gpui::{prelude::*, Node, Render, Stateful, View}; +use gpui::{prelude::*, Div, Render, Stateful, View}; use strum::IntoEnumIterator; use ui::prelude::*; @@ -12,7 +12,7 @@ impl KitchenSinkStory { } impl Render for KitchenSinkStory { - type Element = Stateful>; + type Element = Stateful>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let component_stories = ComponentStory::iter() diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index bba8ae9990..eb4a3b88ea 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,5 +1,5 @@ use fuzzy::StringMatchCandidate; -use gpui::{div, prelude::*, KeyBinding, Node, Render, Styled, Task, View, WindowContext}; +use gpui::{div, prelude::*, KeyBinding, Div, Render, Styled, Task, View, WindowContext}; use picker::{Picker, PickerDelegate}; use std::sync::Arc; use theme2::ActiveTheme; @@ -34,7 +34,7 @@ impl Delegate { } impl PickerDelegate for Delegate { - type ListItem = Node>; + type ListItem = Div>; fn match_count(&self) -> usize { self.candidates.len() @@ -203,7 +203,7 @@ impl PickerStory { } impl Render for PickerStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { div() diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index c5675b5681..c5549e56d0 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -1,5 +1,5 @@ use gpui::{ - div, prelude::*, px, Node, Render, SharedString, Stateful, Styled, View, WindowContext, + div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext, }; use theme2::ActiveTheme; @@ -12,7 +12,7 @@ impl ScrollStory { } impl Render for ScrollStory { - type Element = Stateful>; + type Element = Stateful>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index 86ab2fce99..6fc76ab907 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -1,4 +1,4 @@ -use gpui::{div, white, Node, ParentComponent, Render, Styled, View, VisualContext, WindowContext}; +use gpui::{div, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext}; pub struct TextStory; @@ -9,7 +9,7 @@ impl TextStory { } impl Render for TextStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { div().size_full().bg(white()).child(concat!( diff --git a/crates/storybook2/src/stories/z_index.rs b/crates/storybook2/src/stories/z_index.rs index 259685b9fa..46ec0f4a35 100644 --- a/crates/storybook2/src/stories/z_index.rs +++ b/crates/storybook2/src/stories/z_index.rs @@ -1,4 +1,4 @@ -use gpui::{px, rgb, Hsla, Node, Render}; +use gpui::{px, rgb, Div, Hsla, Render}; use ui::prelude::*; use crate::story::Story; @@ -8,7 +8,7 @@ use crate::story::Story; pub struct ZIndexStory; impl Render for ZIndexStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) @@ -77,7 +77,7 @@ trait Styles: Styled + Sized { } } -impl Styles for Node {} +impl Styles for Div {} #[derive(Component)] struct ZIndexExample { diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index f20aa59095..f0ba124162 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use clap::Parser; use gpui::{ - div, px, size, AnyView, AppContext, Bounds, Node, Render, ViewContext, VisualContext, + div, px, size, AnyView, AppContext, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, WindowOptions, }; use log::LevelFilter; @@ -107,7 +107,7 @@ impl StoryWrapper { } impl Render for StoryWrapper { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/theme2/src/players.rs b/crates/theme2/src/players.rs index 32b3504b65..28489285a7 100644 --- a/crates/theme2/src/players.rs +++ b/crates/theme2/src/players.rs @@ -40,12 +40,12 @@ pub use stories::*; mod stories { use super::*; use crate::{ActiveTheme, Story}; - use gpui::{div, img, px, Node, ParentComponent, Render, Styled, ViewContext}; + use gpui::{div, img, px, Div, ParentComponent, Render, Styled, ViewContext}; pub struct PlayerStory; impl Render for PlayerStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx).child( diff --git a/crates/theme2/src/story.rs b/crates/theme2/src/story.rs index 00cb20df92..4296d4f99c 100644 --- a/crates/theme2/src/story.rs +++ b/crates/theme2/src/story.rs @@ -1,11 +1,11 @@ -use gpui::{div, Component, Node, ParentComponent, Styled, ViewContext}; +use gpui::{div, Component, Div, ParentComponent, Styled, ViewContext}; use crate::ActiveTheme; pub struct Story {} impl Story { - pub fn container(cx: &mut ViewContext) -> Node { + pub fn container(cx: &mut ViewContext) -> Div { div() .size_full() .flex() diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index d270f2fd32..d083d8fd46 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -44,12 +44,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct AvatarStory; impl Render for AvatarStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index eeb8ddb906..1c36eab44f 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -236,13 +236,13 @@ pub use stories::*; mod stories { use super::*; use crate::{h_stack, v_stack, LabelColor, Story}; - use gpui::{rems, Node, Render}; + use gpui::{rems, Div, Render}; use strum::IntoEnumIterator; pub struct ButtonStory; impl Render for ButtonStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let states = InteractionState::iter(); diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs index c2a0cfdc2f..971da8338b 100644 --- a/crates/ui2/src/components/checkbox.rs +++ b/crates/ui2/src/components/checkbox.rs @@ -171,12 +171,12 @@ pub use stories::*; mod stories { use super::*; use crate::{h_stack, Story}; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct CheckboxStory; impl Render for CheckboxStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index a33f10c296..117be12779 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -65,12 +65,12 @@ pub use stories::*; mod stories { use super::*; use crate::story::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct ContextMenuStory; impl Render for ContextMenuStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/details.rs b/crates/ui2/src/components/details.rs index 44c433179a..f138290f17 100644 --- a/crates/ui2/src/components/details.rs +++ b/crates/ui2/src/components/details.rs @@ -47,12 +47,12 @@ pub use stories::*; mod stories { use super::*; use crate::{Button, Story}; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct DetailsStory; impl Render for DetailsStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/elevated_surface.rs b/crates/ui2/src/components/elevated_surface.rs index 3a1699a64a..fff232d7ed 100644 --- a/crates/ui2/src/components/elevated_surface.rs +++ b/crates/ui2/src/components/elevated_surface.rs @@ -1,11 +1,11 @@ -use gpui::Node; +use gpui::Div; use crate::{prelude::*, v_stack}; /// Create an elevated surface. /// /// Must be used inside of a relative parent element -pub fn elevated_surface(level: ElevationIndex, cx: &mut ViewContext) -> Node { +pub fn elevated_surface(level: ElevationIndex, cx: &mut ViewContext) -> Div { let colors = cx.theme().colors(); // let shadow = BoxShadow { @@ -23,6 +23,6 @@ pub fn elevated_surface(level: ElevationIndex, cx: &mut ViewContext< .shadow(level.shadow()) } -pub fn modal(cx: &mut ViewContext) -> Node { +pub fn modal(cx: &mut ViewContext) -> Div { elevated_surface(ElevationIndex::ModalSurface, cx) } diff --git a/crates/ui2/src/components/facepile.rs b/crates/ui2/src/components/facepile.rs index 43fd233458..efac4925f8 100644 --- a/crates/ui2/src/components/facepile.rs +++ b/crates/ui2/src/components/facepile.rs @@ -33,12 +33,12 @@ pub use stories::*; mod stories { use super::*; use crate::{static_players, Story}; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct FacepileStory; impl Render for FacepileStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let players = static_players(); diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index ce980a879b..907f3f9187 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -204,7 +204,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Node, Render}; + use gpui::{Div, Render}; use strum::IntoEnumIterator; use crate::Story; @@ -214,7 +214,7 @@ mod stories { pub struct IconStory; impl Render for IconStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let icons = Icon::iter(); diff --git a/crates/ui2/src/components/input.rs b/crates/ui2/src/components/input.rs index d873d6a5cb..4db9222a0e 100644 --- a/crates/ui2/src/components/input.rs +++ b/crates/ui2/src/components/input.rs @@ -110,12 +110,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct InputStory; impl Render for InputStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 86e876d245..bd02e694ed 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -158,13 +158,13 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; use itertools::Itertools; pub struct KeybindingStory; impl Render for KeybindingStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let all_modifier_permutations = ModifierKey::iter().permutations(2); diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 497ebe6773..827ba87918 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -196,12 +196,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct LabelStory; impl Render for LabelStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 4a753215f0..249e577ff1 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -159,7 +159,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Node, Render}; + use gpui::{Div, Render}; use crate::{ModifierKeys, Story}; @@ -168,7 +168,7 @@ mod stories { pub struct PaletteStory; impl Render for PaletteStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { { diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index 2b317b0bc1..e16f203599 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -126,12 +126,12 @@ pub use stories::*; mod stories { use super::*; use crate::{Label, Story}; - use gpui::{InteractiveComponent, Node, Render}; + use gpui::{InteractiveComponent, Div, Render}; pub struct PanelStory; impl Render for PanelStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/stack.rs b/crates/ui2/src/components/stack.rs index d705863ad4..d3d7a75aa7 100644 --- a/crates/ui2/src/components/stack.rs +++ b/crates/ui2/src/components/stack.rs @@ -1,17 +1,17 @@ -use gpui::{div, Node}; +use gpui::{div, Div}; use crate::StyledExt; /// Horizontally stacks elements. /// /// Sets `flex()`, `flex_row()`, `items_center()` -pub fn h_stack() -> Node { +pub fn h_stack() -> Div { div().h_flex() } /// Vertically stacks elements. /// /// Sets `flex()`, `flex_col()` -pub fn v_stack() -> Node { +pub fn v_stack() -> Div { div().v_flex() } diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index fe993555b9..268098a579 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use crate::{Icon, IconColor, IconElement, Label, LabelColor}; -use gpui::{prelude::*, red, ElementId, Node, Render, View}; +use gpui::{prelude::*, red, Div, ElementId, Render, View}; #[derive(Component, Clone)] pub struct Tab { @@ -21,7 +21,7 @@ struct TabDragState { } impl Render for TabDragState { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div().w_8().h_4().bg(red()) @@ -178,7 +178,7 @@ mod stories { pub struct TabStory; impl Render for TabStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let git_statuses = GitStatus::iter(); diff --git a/crates/ui2/src/components/toast.rs b/crates/ui2/src/components/toast.rs index 2f07bbc3e0..0fcfe6038b 100644 --- a/crates/ui2/src/components/toast.rs +++ b/crates/ui2/src/components/toast.rs @@ -69,7 +69,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Node, Render}; + use gpui::{Div, Render}; use crate::{Label, Story}; @@ -78,7 +78,7 @@ mod stories { pub struct ToastStory; impl Render for ToastStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 6c5fa7bc2b..80c6863f68 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,4 +1,4 @@ -use gpui::{div, Node, ParentComponent, Render, SharedString, Styled, ViewContext}; +use gpui::{div, Div, ParentComponent, Render, SharedString, Styled, ViewContext}; use theme2::ActiveTheme; #[derive(Clone, Debug)] @@ -13,7 +13,7 @@ impl TextTooltip { } impl Render for TextTooltip { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let theme = cx.theme(); diff --git a/crates/ui2/src/story.rs b/crates/ui2/src/story.rs index cf5737a245..94e38267f4 100644 --- a/crates/ui2/src/story.rs +++ b/crates/ui2/src/story.rs @@ -1,11 +1,11 @@ -use gpui::Node; +use gpui::Div; use crate::prelude::*; pub struct Story {} impl Story { - pub fn container(cx: &mut ViewContext) -> Node { + pub fn container(cx: &mut ViewContext) -> Div { div() .size_full() .flex() diff --git a/crates/ui2/src/to_extract/assistant_panel.rs b/crates/ui2/src/to_extract/assistant_panel.rs index 59d9059122..f111dad830 100644 --- a/crates/ui2/src/to_extract/assistant_panel.rs +++ b/crates/ui2/src/to_extract/assistant_panel.rs @@ -77,11 +77,11 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct AssistantPanelStory; impl Render for AssistantPanelStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/breadcrumb.rs b/crates/ui2/src/to_extract/breadcrumb.rs index c113c64168..fd43a5b3bf 100644 --- a/crates/ui2/src/to_extract/breadcrumb.rs +++ b/crates/ui2/src/to_extract/breadcrumb.rs @@ -1,5 +1,5 @@ use crate::{h_stack, prelude::*, HighlightedText}; -use gpui::{prelude::*, Node}; +use gpui::{prelude::*, Div}; use std::path::PathBuf; #[derive(Clone)] @@ -16,7 +16,7 @@ impl Breadcrumb { Self { path, symbols } } - fn render_separator(&self, cx: &WindowContext) -> Node { + fn render_separator(&self, cx: &WindowContext) -> Div { div() .child(" › ") .text_color(cx.theme().colors().text_muted) @@ -77,7 +77,7 @@ mod stories { pub struct BreadcrumbStory; impl Render for BreadcrumbStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/buffer.rs b/crates/ui2/src/to_extract/buffer.rs index b04f2221d3..aa4bebc9d5 100644 --- a/crates/ui2/src/to_extract/buffer.rs +++ b/crates/ui2/src/to_extract/buffer.rs @@ -235,12 +235,12 @@ mod stories { empty_buffer_example, hello_world_rust_buffer_example, hello_world_rust_buffer_with_status_example, Story, }; - use gpui::{rems, Node, Render}; + use gpui::{rems, Div, Render}; pub struct BufferStory; impl Render for BufferStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/buffer_search.rs b/crates/ui2/src/to_extract/buffer_search.rs index 0e6fffa5a7..9993cd3612 100644 --- a/crates/ui2/src/to_extract/buffer_search.rs +++ b/crates/ui2/src/to_extract/buffer_search.rs @@ -1,4 +1,4 @@ -use gpui::{Node, Render, View, VisualContext}; +use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; use crate::{h_stack, Icon, IconButton, IconColor, Input}; @@ -27,9 +27,9 @@ impl BufferSearch { } impl Render for BufferSearch { - type Element = Node; + type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Node { + fn render(&mut self, cx: &mut ViewContext) -> Div { h_stack() .bg(cx.theme().colors().toolbar_background) .p_2() diff --git a/crates/ui2/src/to_extract/chat_panel.rs b/crates/ui2/src/to_extract/chat_panel.rs index 13f35468ff..fcc8e6a46e 100644 --- a/crates/ui2/src/to_extract/chat_panel.rs +++ b/crates/ui2/src/to_extract/chat_panel.rs @@ -107,7 +107,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { use chrono::DateTime; - use gpui::{Node, Render}; + use gpui::{Div, Render}; use crate::{Panel, Story}; @@ -116,7 +116,7 @@ mod stories { pub struct ChatPanelStory; impl Render for ChatPanelStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/collab_panel.rs b/crates/ui2/src/to_extract/collab_panel.rs index d2ac353e05..256a648c0d 100644 --- a/crates/ui2/src/to_extract/collab_panel.rs +++ b/crates/ui2/src/to_extract/collab_panel.rs @@ -93,12 +93,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct CollabPanelStory; impl Render for CollabPanelStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/command_palette.rs b/crates/ui2/src/to_extract/command_palette.rs index 8a9c61490c..8a9461c796 100644 --- a/crates/ui2/src/to_extract/command_palette.rs +++ b/crates/ui2/src/to_extract/command_palette.rs @@ -27,7 +27,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Node, Render}; + use gpui::{Div, Render}; use crate::Story; @@ -36,7 +36,7 @@ mod stories { pub struct CommandPaletteStory; impl Render for CommandPaletteStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/copilot.rs b/crates/ui2/src/to_extract/copilot.rs index 2a2c2c4a27..8750ab3c51 100644 --- a/crates/ui2/src/to_extract/copilot.rs +++ b/crates/ui2/src/to_extract/copilot.rs @@ -25,7 +25,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Node, Render}; + use gpui::{Div, Render}; use crate::Story; @@ -34,7 +34,7 @@ mod stories { pub struct CopilotModalStory; impl Render for CopilotModalStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/editor_pane.rs b/crates/ui2/src/to_extract/editor_pane.rs index 4546c24794..fd21e81242 100644 --- a/crates/ui2/src/to_extract/editor_pane.rs +++ b/crates/ui2/src/to_extract/editor_pane.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use gpui::{Node, Render, View, VisualContext}; +use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; use crate::{ @@ -48,9 +48,9 @@ impl EditorPane { } impl Render for EditorPane { - type Element = Node; + type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Node { + fn render(&mut self, cx: &mut ViewContext) -> Div { v_stack() .w_full() .h_full() diff --git a/crates/ui2/src/to_extract/language_selector.rs b/crates/ui2/src/to_extract/language_selector.rs index 46a10d5c88..694ca78e9c 100644 --- a/crates/ui2/src/to_extract/language_selector.rs +++ b/crates/ui2/src/to_extract/language_selector.rs @@ -40,12 +40,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct LanguageSelectorStory; impl Render for LanguageSelectorStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/multi_buffer.rs b/crates/ui2/src/to_extract/multi_buffer.rs index 0649bf1290..78a22d51d0 100644 --- a/crates/ui2/src/to_extract/multi_buffer.rs +++ b/crates/ui2/src/to_extract/multi_buffer.rs @@ -40,12 +40,12 @@ pub use stories::*; mod stories { use super::*; use crate::{hello_world_rust_buffer_example, Story}; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct MultiBufferStory; impl Render for MultiBufferStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/notifications_panel.rs b/crates/ui2/src/to_extract/notifications_panel.rs index 0a85a15f34..a7854107b1 100644 --- a/crates/ui2/src/to_extract/notifications_panel.rs +++ b/crates/ui2/src/to_extract/notifications_panel.rs @@ -352,12 +352,12 @@ pub use stories::*; mod stories { use super::*; use crate::{Panel, Story}; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct NotificationsPanelStory; impl Render for NotificationsPanelStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/project_panel.rs b/crates/ui2/src/to_extract/project_panel.rs index c55056a7a9..018f9a4bf1 100644 --- a/crates/ui2/src/to_extract/project_panel.rs +++ b/crates/ui2/src/to_extract/project_panel.rs @@ -55,12 +55,12 @@ pub use stories::*; mod stories { use super::*; use crate::{Panel, Story}; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct ProjectPanelStory; impl Render for ProjectPanelStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/recent_projects.rs b/crates/ui2/src/to_extract/recent_projects.rs index 83b15a3128..3d4f551490 100644 --- a/crates/ui2/src/to_extract/recent_projects.rs +++ b/crates/ui2/src/to_extract/recent_projects.rs @@ -36,12 +36,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct RecentProjectsStory; impl Render for RecentProjectsStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/tab_bar.rs b/crates/ui2/src/to_extract/tab_bar.rs index e8de2e9e58..3b4b5cc220 100644 --- a/crates/ui2/src/to_extract/tab_bar.rs +++ b/crates/ui2/src/to_extract/tab_bar.rs @@ -100,12 +100,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct TabBarStory; impl Render for TabBarStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/terminal.rs b/crates/ui2/src/to_extract/terminal.rs index 5bcbca4fde..6c36f35152 100644 --- a/crates/ui2/src/to_extract/terminal.rs +++ b/crates/ui2/src/to_extract/terminal.rs @@ -83,11 +83,11 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Node, Render}; + use gpui::{Div, Render}; pub struct TerminalStory; impl Render for TerminalStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/theme_selector.rs b/crates/ui2/src/to_extract/theme_selector.rs index 7dd169a2f7..7f911b50bf 100644 --- a/crates/ui2/src/to_extract/theme_selector.rs +++ b/crates/ui2/src/to_extract/theme_selector.rs @@ -39,7 +39,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Node, Render}; + use gpui::{Div, Render}; use crate::Story; @@ -48,7 +48,7 @@ mod stories { pub struct ThemeSelectorStory; impl Render for ThemeSelectorStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/title_bar.rs b/crates/ui2/src/to_extract/title_bar.rs index d805992023..87d7dd4146 100644 --- a/crates/ui2/src/to_extract/title_bar.rs +++ b/crates/ui2/src/to_extract/title_bar.rs @@ -1,7 +1,7 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; -use gpui::{Node, Render, View, VisualContext}; +use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; use crate::settings::user_settings; @@ -86,9 +86,9 @@ impl TitleBar { } impl Render for TitleBar { - type Element = Node; + type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Node { + fn render(&mut self, cx: &mut ViewContext) -> Div { let settings = user_settings(cx); // let has_focus = cx.window_is_active(); @@ -202,9 +202,9 @@ mod stories { } impl Render for TitleBarStory { - type Element = Node; + type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Node { + fn render(&mut self, cx: &mut ViewContext) -> Div { Story::container(cx) .child(Story::title_for::<_, TitleBar>(cx)) .child(Story::label(cx, "Default")) diff --git a/crates/ui2/src/to_extract/toolbar.rs b/crates/ui2/src/to_extract/toolbar.rs index 1c1795437b..81918f34a7 100644 --- a/crates/ui2/src/to_extract/toolbar.rs +++ b/crates/ui2/src/to_extract/toolbar.rs @@ -73,7 +73,7 @@ mod stories { use std::path::PathBuf; use std::str::FromStr; - use gpui::{Node, Render}; + use gpui::{Div, Render}; use crate::{Breadcrumb, HighlightedText, Icon, IconButton, Story, Symbol}; @@ -82,7 +82,7 @@ mod stories { pub struct ToolbarStory; impl Render for ToolbarStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/traffic_lights.rs b/crates/ui2/src/to_extract/traffic_lights.rs index 76898eb19a..245ff377f2 100644 --- a/crates/ui2/src/to_extract/traffic_lights.rs +++ b/crates/ui2/src/to_extract/traffic_lights.rs @@ -77,7 +77,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Node, Render}; + use gpui::{Div, Render}; use crate::Story; @@ -86,7 +86,7 @@ mod stories { pub struct TrafficLightsStory; impl Render for TrafficLightsStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container(cx) diff --git a/crates/ui2/src/to_extract/workspace.rs b/crates/ui2/src/to_extract/workspace.rs index 2ab6b4b3ef..d6de8a8288 100644 --- a/crates/ui2/src/to_extract/workspace.rs +++ b/crates/ui2/src/to_extract/workspace.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use chrono::DateTime; -use gpui::{px, relative, Node, Render, Size, View, VisualContext}; +use gpui::{px, relative, Div, Render, Size, View, VisualContext}; use settings2::Settings; use theme2::ThemeSettings; @@ -192,9 +192,9 @@ impl Workspace { } impl Render for Workspace { - type Element = Node; + type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Node { + fn render(&mut self, cx: &mut ViewContext) -> Div { let root_group = PaneGroup::new_panes( vec![Pane::new( "pane-0", @@ -388,7 +388,7 @@ mod stories { } impl Render for WorkspaceStory { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div().child(self.workspace.clone()) diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 0e507c0f7c..7695cffa88 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,6 +1,6 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, Action, AnyView, AppContext, Entity, EntityId, EventEmitter, Node, ParentComponent, + div, Action, AnyView, AppContext, Entity, EntityId, EventEmitter, Div, ParentComponent, Render, Subscription, View, ViewContext, WeakView, WindowContext, }; use schemars::JsonSchema; @@ -419,7 +419,7 @@ impl Dock { } impl Render for Dock { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { todo!() @@ -621,7 +621,7 @@ impl PanelButtons { // } impl Render for PanelButtons { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { // todo!() @@ -647,7 +647,7 @@ impl StatusItemView for PanelButtons { #[cfg(any(test, feature = "test-support"))] pub mod test { use super::*; - use gpui::{div, Node, ViewContext, WindowContext}; + use gpui::{div, Div, ViewContext, WindowContext}; pub struct TestPanel { pub position: DockPosition, @@ -672,7 +672,7 @@ pub mod test { } impl Render for TestPanel { - type Element = Node; + type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 6cc08d56a6..3ebb274969 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,5 +1,5 @@ use gpui::{ - div, prelude::*, px, AnyView, EventEmitter, FocusHandle, Node, Render, Subscription, View, + div, prelude::*, px, AnyView, EventEmitter, FocusHandle, Div, Render, Subscription, View, ViewContext, WindowContext, }; use ui::v_stack; @@ -76,7 +76,7 @@ impl ModalLayer { } impl Render for ModalLayer { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let Some(active_modal) = &self.active_modal else { diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs index 8673645b1e..a17399af57 100644 --- a/crates/workspace2/src/notifications.rs +++ b/crates/workspace2/src/notifications.rs @@ -165,7 +165,7 @@ impl Workspace { pub mod simple_message_notification { use super::{Notification, NotificationEvent}; - use gpui::{AnyElement, AppContext, EventEmitter, Node, Render, TextStyle, ViewContext}; + use gpui::{AnyElement, AppContext, EventEmitter, Div, Render, TextStyle, ViewContext}; use serde::Deserialize; use std::{borrow::Cow, sync::Arc}; @@ -252,7 +252,7 @@ pub mod simple_message_notification { } impl Render for MessageNotification { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { todo!() diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index ff16ebdc6d..c4935599a6 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -8,7 +8,7 @@ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, EntityId, - EventEmitter, FocusHandle, Model, Node, PromptLevel, Render, Task, View, ViewContext, + EventEmitter, FocusHandle, Model, Div, PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; @@ -1901,7 +1901,7 @@ impl Pane { // } impl Render for Pane { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { v_stack() @@ -2926,7 +2926,7 @@ struct DraggedTab { } impl Render for DraggedTab { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div().w_8().h_4().bg(gpui::red()) diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index 6ac524f824..18c6bbf079 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -2,7 +2,7 @@ use std::any::TypeId; use crate::{ItemHandle, Pane}; use gpui::{ - div, AnyView, Component, Node, ParentComponent, Render, Styled, Subscription, View, + div, AnyView, Component, Div, ParentComponent, Render, Styled, Subscription, View, ViewContext, WindowContext, }; use theme2::ActiveTheme; @@ -34,7 +34,7 @@ pub struct StatusBar { } impl Render for StatusBar { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index dfe661d5b0..1d67da06b2 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -1,6 +1,6 @@ use crate::ItemHandle; use gpui::{ - AnyView, Entity, EntityId, EventEmitter, Node, Render, View, ViewContext, WindowContext, + AnyView, Div, Entity, EntityId, EventEmitter, Render, View, ViewContext, WindowContext, }; pub enum ToolbarItemEvent { @@ -52,7 +52,7 @@ pub struct Toolbar { } impl Render for Toolbar { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { todo!() diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index df2b1d8f30..10399624b5 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -38,7 +38,7 @@ use futures::{ use gpui::{ actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Entity, EntityId, - EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, Node, + EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, Div, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; @@ -529,7 +529,7 @@ pub enum Event { pub struct Workspace { weak_self: WeakView, focus_handle: FocusHandle, - workspace_actions: Vec) -> Node>>, + workspace_actions: Vec) -> Div>>, zoomed: Option, zoomed_position: Option, center: PaneGroup, @@ -3503,7 +3503,7 @@ impl Workspace { })); } - fn add_workspace_actions_listeners(&self, mut div: Node) -> Node { + fn add_workspace_actions_listeners(&self, mut div: Div) -> Div { for action in self.workspace_actions.iter() { div = (action)(div) } @@ -3728,7 +3728,7 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA impl EventEmitter for Workspace {} impl Render for Workspace { - type Element = Node; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); From 6abaacc457ceae2b521e26bc1e5de33964a72cfa Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 01:58:10 -0700 Subject: [PATCH 081/126] Fix formatting --- crates/storybook2/src/stories/focus.rs | 2 +- crates/storybook2/src/stories/picker.rs | 2 +- crates/storybook2/src/stories/scroll.rs | 4 +--- crates/ui2/src/components/panel.rs | 2 +- crates/ui2/src/prelude.rs | 4 ++-- crates/workspace2/src/dock.rs | 4 ++-- crates/workspace2/src/modal_layer.rs | 2 +- crates/workspace2/src/notifications.rs | 2 +- crates/workspace2/src/pane.rs | 6 +++--- crates/workspace2/src/status_bar.rs | 4 ++-- crates/workspace2/src/workspace2.rs | 8 ++++---- 11 files changed, 19 insertions(+), 21 deletions(-) diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index ff9a4da876..a8794afdb8 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -1,5 +1,5 @@ use gpui::{ - actions, div, prelude::*, FocusHandle, Focusable, KeyBinding, Div, Render, Stateful, View, + actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View, WindowContext, }; use theme2::ActiveTheme; diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index eb4a3b88ea..a3f9ef5eb8 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,5 +1,5 @@ use fuzzy::StringMatchCandidate; -use gpui::{div, prelude::*, KeyBinding, Div, Render, Styled, Task, View, WindowContext}; +use gpui::{div, prelude::*, Div, KeyBinding, Render, Styled, Task, View, WindowContext}; use picker::{Picker, PickerDelegate}; use std::sync::Arc; use theme2::ActiveTheme; diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index c5549e56d0..f9530269d5 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -1,6 +1,4 @@ -use gpui::{ - div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext, -}; +use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext}; use theme2::ActiveTheme; pub struct ScrollStory; diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index e16f203599..d9fc50dd92 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -126,7 +126,7 @@ pub use stories::*; mod stories { use super::*; use crate::{Label, Story}; - use gpui::{InteractiveComponent, Div, Render}; + use gpui::{Div, InteractiveComponent, Render}; pub struct PanelStory; diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index f37b6123e3..09ce43d912 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -1,8 +1,8 @@ use gpui::rems; use gpui::Rems; pub use gpui::{ - div, Component, Element, ElementId, InteractiveComponent, ParentComponent, SharedString, Styled, - ViewContext, WindowContext, + div, Component, Element, ElementId, InteractiveComponent, ParentComponent, SharedString, + Styled, ViewContext, WindowContext, }; pub use crate::elevation::*; diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 7695cffa88..499fb0d673 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,7 +1,7 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, Action, AnyView, AppContext, Entity, EntityId, EventEmitter, Div, ParentComponent, - Render, Subscription, View, ViewContext, WeakView, WindowContext, + div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, ParentComponent, Render, + Subscription, View, ViewContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 0703d0dc56..c9dddfdace 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,5 +1,5 @@ use gpui::{ - div, prelude::*, px, AnyView, EventEmitter, FocusHandle, Div, Render, Subscription, View, + div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View, ViewContext, WindowContext, }; use ui::{h_stack, v_stack}; diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs index a17399af57..7277cc6fc4 100644 --- a/crates/workspace2/src/notifications.rs +++ b/crates/workspace2/src/notifications.rs @@ -165,7 +165,7 @@ impl Workspace { pub mod simple_message_notification { use super::{Notification, NotificationEvent}; - use gpui::{AnyElement, AppContext, EventEmitter, Div, Render, TextStyle, ViewContext}; + use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext}; use serde::Deserialize; use std::{borrow::Cow, sync::Arc}; diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index aeca617342..de07847583 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -7,9 +7,9 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, EntityId, - EventEmitter, FocusHandle, Model, Div, PromptLevel, Render, Task, View, ViewContext, - VisualContext, WeakView, WindowContext, + actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId, + EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, + WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index 18c6bbf079..5dccac243f 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -2,8 +2,8 @@ use std::any::TypeId; use crate::{ItemHandle, Pane}; use gpui::{ - div, AnyView, Component, Div, ParentComponent, Render, Styled, Subscription, View, - ViewContext, WindowContext, + div, AnyView, Component, Div, ParentComponent, Render, Styled, Subscription, View, ViewContext, + WindowContext, }; use theme2::ActiveTheme; use util::ResultExt; diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index b48e4aa278..3e3312ac3a 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -37,10 +37,10 @@ use futures::{ }; use gpui::{ actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView, - AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Entity, EntityId, - EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, Div, - ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, - WindowBounds, WindowContext, WindowHandle, WindowOptions, + AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, + EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, + Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, + WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; From 516a8790b9e790e3b863f8242fa55b2708ac309a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 14 Nov 2023 08:28:57 -0500 Subject: [PATCH 082/126] Add gpt-4-1106-preview model --- Cargo.lock | 5 ++--- Cargo.toml | 3 +++ assets/settings/default.json | 14 ++++---------- crates/Cargo.toml | 2 +- crates/ai/Cargo.toml | 2 +- crates/ai2/Cargo.toml | 2 +- crates/assistant/Cargo.toml | 2 +- crates/assistant/src/assistant_settings.rs | 7 ++++++- crates/semantic_index/Cargo.toml | 2 +- 9 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97653e124a..d61fe45cf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9256,9 +9256,8 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9ae5a3c24361e5f038af22517ba7f8e3af4099e30e78a3d56f86b48238fce9d" +version = "0.5.6" +source = "git+https://github.com/JosephTLyons/tiktoken-rs/?rev=edb3ea9eda1b906205b346599c43c5c0e8da1392#edb3ea9eda1b906205b346599c43c5c0e8da1392" dependencies = [ "anyhow", "base64 0.21.4", diff --git a/Cargo.toml b/Cargo.toml index 905750f835..2e43fa45d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,6 +160,9 @@ pretty_assertions = "1.3.0" git2 = { version = "0.15", default-features = false} uuid = { version = "1.1.2", features = ["v4"] } +# Point back to original crate when this is merged: +# https://github.com/zurawiki/tiktoken-rs/pull/49 +tiktoken-rs = { git = "https://github.com/JosephTLyons/tiktoken-rs/", rev="edb3ea9eda1b906205b346599c43c5c0e8da1392" } tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" } tree-sitter-c = "0.20.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" } diff --git a/assets/settings/default.json b/assets/settings/default.json index 42f3b31286..85f8a8fbc4 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -174,7 +174,8 @@ // // 1. "gpt-3.5-turbo-0613"" // 2. "gpt-4-0613"" - "default_open_ai_model": "gpt-4-0613" + // 3. "gpt-4-1106-preview" + "default_open_ai_model": "gpt-4-1106-preview" }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, @@ -270,9 +271,7 @@ "copilot": { // The set of glob patterns for which copilot should be disabled // in any matching file. - "disabled_globs": [ - ".env" - ] + "disabled_globs": [".env"] }, // Settings specific to journaling "journal": { @@ -381,12 +380,7 @@ // Default directories to search for virtual environments, relative // to the current working directory. We recommend overriding this // in your project's settings, rather than globally. - "directories": [ - ".env", - "env", - ".venv", - "venv" - ], + "directories": [".env", "env", ".venv", "venv"], // Can also be 'csh', 'fish', and `nushell` "activate_script": "default" } diff --git a/crates/Cargo.toml b/crates/Cargo.toml index fb49a4b515..6516e07cd4 100644 --- a/crates/Cargo.toml +++ b/crates/Cargo.toml @@ -29,7 +29,7 @@ postage.workspace = true rand.workspace = true log.workspace = true parse_duration = "2.1.1" -tiktoken-rs = "0.5.0" +tiktoken-rs.workspace = true matrixmultiply = "0.3.7" rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } bincode = "1.3.3" diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index fb49a4b515..6516e07cd4 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -29,7 +29,7 @@ postage.workspace = true rand.workspace = true log.workspace = true parse_duration = "2.1.1" -tiktoken-rs = "0.5.0" +tiktoken-rs.workspace = true matrixmultiply = "0.3.7" rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } bincode = "1.3.3" diff --git a/crates/ai2/Cargo.toml b/crates/ai2/Cargo.toml index aee265db6e..25c9965915 100644 --- a/crates/ai2/Cargo.toml +++ b/crates/ai2/Cargo.toml @@ -29,7 +29,7 @@ postage.workspace = true rand.workspace = true log.workspace = true parse_duration = "2.1.1" -tiktoken-rs = "0.5.0" +tiktoken-rs.workspace = true matrixmultiply = "0.3.7" rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } bincode = "1.3.3" diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index fc885f6b36..876e5e0b76 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -40,7 +40,7 @@ schemars.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true -tiktoken-rs = "0.5" +tiktoken-rs.workspace = true [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs index 05d8d9ffeb..65dd588b3c 100644 --- a/crates/assistant/src/assistant_settings.rs +++ b/crates/assistant/src/assistant_settings.rs @@ -9,6 +9,8 @@ pub enum OpenAIModel { ThreePointFiveTurbo, #[serde(rename = "gpt-4-0613")] Four, + #[serde(rename = "gpt-4-1106-preview")] + FourTurbo, } impl OpenAIModel { @@ -16,6 +18,7 @@ impl OpenAIModel { match self { OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613", OpenAIModel::Four => "gpt-4-0613", + OpenAIModel::FourTurbo => "gpt-4-1106-preview", } } @@ -23,13 +26,15 @@ impl OpenAIModel { match self { OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo", OpenAIModel::Four => "gpt-4", + OpenAIModel::FourTurbo => "gpt-4-turbo", } } pub fn cycle(&self) -> Self { match self { OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four, - OpenAIModel::Four => OpenAIModel::ThreePointFiveTurbo, + OpenAIModel::Four => OpenAIModel::FourTurbo, + OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo, } } } diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 875440ef3f..0308927944 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -33,7 +33,7 @@ lazy_static.workspace = true serde.workspace = true serde_json.workspace = true async-trait.workspace = true -tiktoken-rs = "0.5.0" +tiktoken-rs.workspace = true parking_lot.workspace = true rand.workspace = true schemars.workspace = true From fc5ec47cc8bce50fc719b0638763a72d12148ea6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 10 Nov 2023 17:16:33 +0100 Subject: [PATCH 083/126] WIP --- crates/editor2/src/display_map/block_map.rs | 13 +- crates/editor2/src/element.rs | 436 +++++++++----------- crates/ui2/src/components/icon_button.rs | 16 +- 3 files changed, 218 insertions(+), 247 deletions(-) diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs index aa5ff0e3d2..2f65903f08 100644 --- a/crates/editor2/src/display_map/block_map.rs +++ b/crates/editor2/src/display_map/block_map.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; -use gpui::{AnyElement, ViewContext}; +use gpui::{AnyElement, Pixels, ViewContext}; use language::{BufferSnapshot, Chunk, Patch, Point}; use parking_lot::Mutex; use std::{ @@ -82,12 +82,11 @@ pub enum BlockStyle { pub struct BlockContext<'a, 'b> { pub view_context: &'b mut ViewContext<'a, Editor>, - pub anchor_x: f32, - pub scroll_x: f32, - pub gutter_width: f32, - pub gutter_padding: f32, - pub em_width: f32, - pub line_height: f32, + pub anchor_x: Pixels, + pub gutter_width: Pixels, + pub gutter_padding: Pixels, + pub em_width: Pixels, + pub line_height: Pixels, pub block_id: usize, } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 65f6edb18d..8a1385cf93 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1,5 +1,8 @@ use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint}, + display_map::{ + BlockContext, BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint, + TransformBlock, + }, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::hover_at, @@ -15,17 +18,18 @@ use crate::{ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ - black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, - BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, - ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, - KeyContext, KeyDownEvent, KeyMatch, Line, LineLayout, Modifiers, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size, Style, TextRun, - TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout, + point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, + Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, + Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, + Pixels, ScrollWheelEvent, Size, Style, TextRun, TextStyle, ViewContext, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; use multi_buffer::Anchor; -use project::project_settings::{GitGutterSetting, ProjectSettings}; +use project::{ + project_settings::{GitGutterSetting, ProjectSettings}, + ProjectPath, +}; use settings::Settings; use smallvec::SmallVec; use std::{ @@ -39,6 +43,7 @@ use std::{ }; use sum_tree::Bias; use theme::{ActiveTheme, PlayerColor}; +use ui::{h_stack, IconButton}; use util::ResultExt; use workspace::item::Item; @@ -1741,22 +1746,22 @@ impl EditorElement { .unwrap() .width; let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; - // todo!("blocks") - // let (scroll_width, blocks) = self.layout_blocks( - // start_row..end_row, - // &snapshot, - // size.x, - // scroll_width, - // gutter_padding, - // gutter_width, - // em_width, - // gutter_width + gutter_margin, - // line_height, - // &style, - // &line_layouts, - // editor, - // cx, - // ); + + let (scroll_width, blocks) = self.layout_blocks( + start_row..end_row, + &snapshot, + bounds.size.width, + scroll_width, + gutter_padding, + gutter_width, + em_width, + gutter_width + gutter_margin, + line_height, + &style, + &line_layouts, + editor, + cx, + ); let scroll_max = point( f32::from((scroll_width - text_size.width) / em_width).max(0.0), @@ -1948,226 +1953,181 @@ impl EditorElement { } } - // #[allow(clippy::too_many_arguments)] - // fn layout_blocks( - // &mut self, - // rows: Range, - // snapshot: &EditorSnapshot, - // editor_width: f32, - // scroll_width: f32, - // gutter_padding: f32, - // gutter_width: f32, - // em_width: f32, - // text_x: f32, - // line_height: f32, - // style: &EditorStyle, - // line_layouts: &[LineWithInvisibles], - // editor: &mut Editor, - // cx: &mut ViewContext, - // ) -> (f32, Vec) { - // let mut block_id = 0; - // let scroll_x = snapshot.scroll_anchor.offset.x; - // let (fixed_blocks, non_fixed_blocks) = snapshot - // .blocks_in_range(rows.clone()) - // .partition::, _>(|(_, block)| match block { - // TransformBlock::ExcerptHeader { .. } => false, - // TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, - // }); - // let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| { - // let mut element = match block { - // TransformBlock::Custom(block) => { - // let align_to = block - // .position() - // .to_point(&snapshot.buffer_snapshot) - // .to_display_point(snapshot); - // let anchor_x = text_x - // + if rows.contains(&align_to.row()) { - // line_layouts[(align_to.row() - rows.start) as usize] - // .line - // .x_for_index(align_to.column() as usize) - // } else { - // layout_line(align_to.row(), snapshot, style, cx.text_layout_cache()) - // .x_for_index(align_to.column() as usize) - // }; + #[allow(clippy::too_many_arguments)] + fn layout_blocks( + &mut self, + rows: Range, + snapshot: &EditorSnapshot, + editor_width: Pixels, + scroll_width: Pixels, + gutter_padding: Pixels, + gutter_width: Pixels, + em_width: Pixels, + text_x: Pixels, + line_height: Pixels, + style: &EditorStyle, + line_layouts: &[LineWithInvisibles], + editor: &mut Editor, + cx: &mut ViewContext, + ) -> (Pixels, Vec) { + let mut block_id = 0; + let scroll_x = snapshot.scroll_anchor.offset.x; + let (fixed_blocks, non_fixed_blocks) = snapshot + .blocks_in_range(rows.clone()) + .partition::, _>(|(_, block)| match block { + TransformBlock::ExcerptHeader { .. } => false, + TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, + }); + let mut render_block = |block: &TransformBlock, width: Pixels, block_id: usize| { + let mut element = match block { + TransformBlock::Custom(block) => { + let align_to = block + .position() + .to_point(&snapshot.buffer_snapshot) + .to_display_point(snapshot); + let anchor_x = text_x + + if rows.contains(&align_to.row()) { + line_layouts[(align_to.row() - rows.start) as usize] + .line + .x_for_index(align_to.column() as usize) + } else { + layout_line(align_to.row(), snapshot, style, cx) + .unwrap() + .x_for_index(align_to.column() as usize) + }; - // block.render(&mut BlockContext { - // view_context: cx, - // anchor_x, - // gutter_padding, - // line_height, - // scroll_x, - // gutter_width, - // em_width, - // block_id, - // }) - // } - // TransformBlock::ExcerptHeader { - // id, - // buffer, - // range, - // starts_new_buffer, - // .. - // } => { - // let tooltip_style = theme::current(cx).tooltip.clone(); - // let include_root = editor - // .project - // .as_ref() - // .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) - // .unwrap_or_default(); - // let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { - // let jump_path = ProjectPath { - // worktree_id: file.worktree_id(cx), - // path: file.path.clone(), - // }; - // let jump_anchor = range - // .primary - // .as_ref() - // .map_or(range.context.start, |primary| primary.start); - // let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); + block.render(&mut BlockContext { + view_context: cx, + anchor_x, + gutter_padding, + line_height, + // scroll_x, + gutter_width, + em_width, + block_id, + }) + } + TransformBlock::ExcerptHeader { + id, + buffer, + range, + starts_new_buffer, + .. + } => { + let include_root = editor + .project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(); + let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { + let jump_path = ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }; + let jump_anchor = range + .primary + .as_ref() + .map_or(range.context.start, |primary| primary.start); + let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); - // enum JumpIcon {} - // MouseEventHandler::new::((*id).into(), cx, |state, _| { - // let style = style.jump_icon.style_for(state); - // Svg::new("icons/arrow_up_right.svg") - // .with_color(style.color) - // .constrained() - // .with_width(style.icon_width) - // .aligned() - // .contained() - // .with_style(style.container) - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, editor, cx| { - // if let Some(workspace) = editor - // .workspace - // .as_ref() - // .and_then(|(workspace, _)| workspace.upgrade(cx)) - // { - // workspace.update(cx, |workspace, cx| { - // Editor::jump( - // workspace, - // jump_path.clone(), - // jump_position, - // jump_anchor, - // cx, - // ); - // }); - // } - // }) - // .with_tooltip::( - // (*id).into(), - // "Jump to Buffer".to_string(), - // Some(Box::new(crate::OpenExcerpts)), - // tooltip_style.clone(), - // cx, - // ) - // .aligned() - // .flex_float() - // }); + // todo!("avoid ElementId collision risk here") + IconButton::new(usize::from(*id), ui::Icon::ArrowUpRight) + .on_click(move |editor, cx| { + if let Some(workspace) = editor + .workspace + .as_ref() + .and_then(|(workspace, _)| workspace.upgrade(cx)) + { + workspace.update(cx, |workspace, cx| { + Editor::jump( + workspace, + jump_path.clone(), + jump_position, + jump_anchor, + cx, + ); + }); + } + }) + .tooltip("Jump to Buffer") // todo!(pass an action as well to show key binding) + }); - // if *starts_new_buffer { - // let editor_font_size = style.text.font_size; - // let style = &style.diagnostic_path_header; - // let font_size = (style.text_scale_factor * editor_font_size).round(); + if *starts_new_buffer { + let path = buffer.resolve_file_path(cx, include_root); + let mut filename = None; + let mut parent_path = None; + // Can't use .and_then() because `.file_name()` and `.parent()` return references :( + if let Some(path) = path { + filename = path.file_name().map(|f| f.to_string_lossy().to_string()); + parent_path = + path.parent().map(|p| p.to_string_lossy().to_string() + "/"); + } - // let path = buffer.resolve_file_path(cx, include_root); - // let mut filename = None; - // let mut parent_path = None; - // // Can't use .and_then() because `.file_name()` and `.parent()` return references :( - // if let Some(path) = path { - // filename = path.file_name().map(|f| f.to_string_lossy.to_string()); - // parent_path = - // path.parent().map(|p| p.to_string_lossy.to_string() + "/"); - // } + h_stack() + .child(filename.unwrap_or_else(|| "untitled".to_string())) + .children(parent_path) + .children(jump_icon) + .p_x(gutter_padding) + } else { + let text_style = style.text.clone(); + h_stack() + .child("⋯") + .children(jump_icon) + .p_x(gutter_padding) + .expanded() + .into_any_named("collapsed context") + } + } + }; - // Flex::row() - // .with_child( - // Label::new( - // filename.unwrap_or_else(|| "untitled".to_string()), - // style.filename.text.clone().with_font_size(font_size), - // ) - // .contained() - // .with_style(style.filename.container) - // .aligned(), - // ) - // .with_children(parent_path.map(|path| { - // Label::new(path, style.path.text.clone().with_font_size(font_size)) - // .contained() - // .with_style(style.path.container) - // .aligned() - // })) - // .with_children(jump_icon) - // .contained() - // .with_style(style.container) - // .with_padding_left(gutter_padding) - // .with_padding_right(gutter_padding) - // .expanded() - // .into_any_named("path header block") - // } else { - // let text_style = style.text.clone(); - // Flex::row() - // .with_child(Label::new("⋯", text_style)) - // .with_children(jump_icon) - // .contained() - // .with_padding_left(gutter_padding) - // .with_padding_right(gutter_padding) - // .expanded() - // .into_any_named("collapsed context") - // } - // } - // }; + // element.layout( + // SizeConstraint { + // min: gpui::Point::::zero(), + // max: point(width, block.height() as f32 * line_height), + // }, + // editor, + // cx, + // ); + element + }; - // element.layout( - // SizeConstraint { - // min: gpui::Point::::zero(), - // max: point(width, block.height() as f32 * line_height), - // }, - // editor, - // cx, - // ); - // element - // }; - - // let mut fixed_block_max_width = 0f32; - // let mut blocks = Vec::new(); - // for (row, block) in fixed_blocks { - // let element = render_block(block, f32::INFINITY, block_id); - // block_id += 1; - // fixed_block_max_width = fixed_block_max_width.max(element.size().x + em_width); - // blocks.push(BlockLayout { - // row, - // element, - // style: BlockStyle::Fixed, - // }); - // } - // for (row, block) in non_fixed_blocks { - // let style = match block { - // TransformBlock::Custom(block) => block.style(), - // TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky, - // }; - // let width = match style { - // BlockStyle::Sticky => editor_width, - // BlockStyle::Flex => editor_width - // .max(fixed_block_max_width) - // .max(gutter_width + scroll_width), - // BlockStyle::Fixed => unreachable!(), - // }; - // let element = render_block(block, width, block_id); - // block_id += 1; - // blocks.push(BlockLayout { - // row, - // element, - // style, - // }); - // } - // ( - // scroll_width.max(fixed_block_max_width - gutter_width), - // blocks, - // ) - // } + let mut fixed_block_max_width = Pixels::ZERO; + let mut blocks = Vec::new(); + for (row, block) in fixed_blocks { + let element = render_block(block, f32::INFINITY, block_id); + block_id += 1; + fixed_block_max_width = fixed_block_max_width.max(element.size().x + em_width); + blocks.push(BlockLayout { + row, + element, + style: BlockStyle::Fixed, + }); + } + for (row, block) in non_fixed_blocks { + let style = match block { + TransformBlock::Custom(block) => block.style(), + TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky, + }; + let width = match style { + BlockStyle::Sticky => editor_width, + BlockStyle::Flex => editor_width + .max(fixed_block_max_width) + .max(gutter_width + scroll_width), + BlockStyle::Fixed => unreachable!(), + }; + let element = render_block(block, width, block_id); + block_id += 1; + blocks.push(BlockLayout { + row, + element, + style, + }); + } + ( + scroll_width.max(fixed_block_max_width - gutter_width), + blocks, + ) + } fn paint_mouse_listeners( &mut self, diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 91653ea8cd..f093804aa8 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,8 +1,8 @@ use std::sync::Arc; -use gpui::{rems, MouseButton}; +use gpui::{rems, MouseButton, VisualContext}; -use crate::{h_stack, prelude::*}; +use crate::{h_stack, prelude::*, TextTooltip}; use crate::{ClickHandler, Icon, IconColor, IconElement}; struct IconButtonHandlers { @@ -22,6 +22,7 @@ pub struct IconButton { color: IconColor, variant: ButtonVariant, state: InteractionState, + tooltip: Option, handlers: IconButtonHandlers, } @@ -33,6 +34,7 @@ impl IconButton { color: IconColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), + tooltip: None, handlers: IconButtonHandlers::default(), } } @@ -57,6 +59,11 @@ impl IconButton { self } + pub fn tooltip(mut self, tooltip: impl Into) -> Self { + self.tooltip = Some(tooltip.into()); + self + } + pub fn on_click( mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext) + Send + Sync, @@ -103,6 +110,11 @@ impl IconButton { }); } + if let Some(tooltip) = self.tooltip.clone() { + button = + button.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(tooltip.clone()))); + } + button } } From 946a696d3de3fbbfc91a120a24f3f78122249aac Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 14 Nov 2023 09:00:04 -0500 Subject: [PATCH 084/126] Update `tiktoken-rs` dependency The PR to add the `gpt-4-1106-preview` model was merged: https://github.com/zurawiki/tiktoken-rs/pull/49 --- Cargo.lock | 2 +- Cargo.toml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d61fe45cf8..1b1758dd8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9257,7 +9257,7 @@ dependencies = [ [[package]] name = "tiktoken-rs" version = "0.5.6" -source = "git+https://github.com/JosephTLyons/tiktoken-rs/?rev=edb3ea9eda1b906205b346599c43c5c0e8da1392#edb3ea9eda1b906205b346599c43c5c0e8da1392" +source = "git+https://github.com/zurawiki/tiktoken-rs?rev=6fd80d41d5c31e256cd760b52cd0257586033eb2#6fd80d41d5c31e256cd760b52cd0257586033eb2" dependencies = [ "anyhow", "base64 0.21.4", diff --git a/Cargo.toml b/Cargo.toml index 2e43fa45d0..a04a6d10b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,9 +160,7 @@ pretty_assertions = "1.3.0" git2 = { version = "0.15", default-features = false} uuid = { version = "1.1.2", features = ["v4"] } -# Point back to original crate when this is merged: -# https://github.com/zurawiki/tiktoken-rs/pull/49 -tiktoken-rs = { git = "https://github.com/JosephTLyons/tiktoken-rs/", rev="edb3ea9eda1b906205b346599c43c5c0e8da1392" } +tiktoken-rs = { git = "https://github.com/zurawiki/tiktoken-rs", rev="6fd80d41d5c31e256cd760b52cd0257586033eb2" } tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" } tree-sitter-c = "0.20.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" } From a5fc5819f12d5726dc9a0e80ac52674f9225157c Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 14 Nov 2023 09:12:25 -0500 Subject: [PATCH 085/126] Bump `tiktoken-rs` to official release --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b1758dd8a..0882435df9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9256,8 +9256,9 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.5.6" -source = "git+https://github.com/zurawiki/tiktoken-rs?rev=6fd80d41d5c31e256cd760b52cd0257586033eb2#6fd80d41d5c31e256cd760b52cd0257586033eb2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4427b6b1c6b38215b92dd47a83a0ecc6735573d0a5a4c14acc0ac5b33b28adb" dependencies = [ "anyhow", "base64 0.21.4", diff --git a/Cargo.toml b/Cargo.toml index a04a6d10b5..8434acdfd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,13 +154,13 @@ tempdir = { version = "0.3.7" } thiserror = { version = "1.0.29" } time = { version = "0.3", features = ["serde", "serde-well-known"] } toml = { version = "0.5" } +tiktoken-rs = "0.5.7" tree-sitter = "0.20" unindent = { version = "0.1.7" } pretty_assertions = "1.3.0" git2 = { version = "0.15", default-features = false} uuid = { version = "1.1.2", features = ["v4"] } -tiktoken-rs = { git = "https://github.com/zurawiki/tiktoken-rs", rev="6fd80d41d5c31e256cd760b52cd0257586033eb2" } tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" } tree-sitter-c = "0.20.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" } From f9b9b7549f6f5d8b34d10b4c61975848d1e78f7b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 16:03:06 +0100 Subject: [PATCH 086/126] Render block elements Co-Authored-By: Julia --- crates/editor2/src/editor.rs | 139 ++++++++++++--------------- crates/editor2/src/element.rs | 136 +++++++++++++------------- crates/ui2/src/components/tooltip.rs | 6 +- 3 files changed, 132 insertions(+), 149 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index fe98dd8679..22ceea51a3 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -22,7 +22,7 @@ mod editor_tests; pub mod test; use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; -use anyhow::{Context as _, Result}; +use anyhow::{anyhow, Context as _, Result}; use blink_manager::BlinkManager; use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings}; use clock::ReplicaId; @@ -43,8 +43,8 @@ use gpui::{ AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, - StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, - ViewContext, VisualContext, WeakView, WindowContext, + StatefulInteractive, StatelessInteractive, Styled, Subscription, Task, TextStyle, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -69,7 +69,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; -use project::{FormatTrigger, Location, Project, ProjectTransaction}; +use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::prelude::*; use rpc::proto::*; use scroll::{ @@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; -use ui::{IconButton, StyledExt}; +use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, @@ -8869,46 +8869,50 @@ impl Editor { // }); // } - // fn jump( - // workspace: &mut Workspace, - // path: ProjectPath, - // position: Point, - // anchor: language::Anchor, - // cx: &mut ViewContext, - // ) { - // let editor = workspace.open_path(path, None, true, cx); - // cx.spawn(|_, mut cx| async move { - // let editor = editor - // .await? - // .downcast::() - // .ok_or_else(|| anyhow!("opened item was not an editor"))? - // .downgrade(); - // editor.update(&mut cx, |editor, cx| { - // let buffer = editor - // .buffer() - // .read(cx) - // .as_singleton() - // .ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?; - // let buffer = buffer.read(cx); - // let cursor = if buffer.can_resolve(&anchor) { - // language::ToPoint::to_point(&anchor, buffer) - // } else { - // buffer.clip_point(position, Bias::Left) - // }; + fn jump( + &mut self, + path: ProjectPath, + position: Point, + anchor: language::Anchor, + cx: &mut ViewContext, + ) { + let workspace = self.workspace(); + cx.spawn(|_, mut cx| async move { + let workspace = workspace.ok_or_else(|| anyhow!("cannot jump without workspace"))?; + let editor = workspace.update(&mut cx, |workspace, cx| { + workspace.open_path(path, None, true, cx) + })?; + let editor = editor + .await? + .downcast::() + .ok_or_else(|| anyhow!("opened item was not an editor"))? + .downgrade(); + editor.update(&mut cx, |editor, cx| { + let buffer = editor + .buffer() + .read(cx) + .as_singleton() + .ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?; + let buffer = buffer.read(cx); + let cursor = if buffer.can_resolve(&anchor) { + language::ToPoint::to_point(&anchor, buffer) + } else { + buffer.clip_point(position, Bias::Left) + }; - // let nav_history = editor.nav_history.take(); - // editor.change_selections(Some(Autoscroll::newest()), cx, |s| { - // s.select_ranges([cursor..cursor]); - // }); - // editor.nav_history = nav_history; + let nav_history = editor.nav_history.take(); + editor.change_selections(Some(Autoscroll::newest()), cx, |s| { + s.select_ranges([cursor..cursor]); + }); + editor.nav_history = nav_history; - // anyhow::Ok(()) - // })??; + anyhow::Ok(()) + })??; - // anyhow::Ok(()) - // }) - // .detach_and_log_err(cx); - // } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } fn marked_text_ranges(&self, cx: &AppContext) -> Option>> { let snapshot = self.buffer.read(cx).read(cx); @@ -9973,43 +9977,20 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend } let message = diagnostic.message; Arc::new(move |cx: &mut BlockContext| { - todo!() - // let message = message.clone(); - // let settings = ThemeSettings::get_global(cx); - // let tooltip_style = settings.theme.tooltip.clone(); - // let theme = &settings.theme.editor; - // let style = diagnostic_style(diagnostic.severity, is_valid, theme); - // let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); - // let anchor_x = cx.anchor_x; - // enum BlockContextToolip {} - // MouseEventHandler::new::(cx.block_id, cx, |_, _| { - // Flex::column() - // .with_children(highlighted_lines.iter().map(|(line, highlights)| { - // Label::new( - // line.clone(), - // style.message.clone().with_font_size(font_size), - // ) - // .with_highlights(highlights.clone()) - // .contained() - // .with_margin_left(anchor_x) - // })) - // .aligned() - // .left() - // .into_any() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // cx.write_to_clipboard(ClipboardItem::new(message.clone())); - // }) - // // We really need to rethink this ID system... - // .with_tooltip::( - // cx.block_id, - // "Copy diagnostic message", - // None, - // tooltip_style, - // cx, - // ) - // .into_any() + let message = message.clone(); + v_stack() + .id(cx.block_id) + .children(highlighted_lines.iter().map(|(line, highlights)| { + div() + .child(HighlightedLabel::new(line.clone(), highlights.clone())) + .ml(cx.anchor_x) + })) + .cursor_pointer() + .on_click(move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new(message.clone())); + }) + .tooltip(|_, cx| cx.build_view(|cx| TextTooltip::new("Copy diagnostic message"))) + .render() }) } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 8a1385cf93..64a281d9e2 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -19,9 +19,10 @@ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, - Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, - Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, - Pixels, ScrollWheelEvent, Size, Style, TextRun, TextStyle, ViewContext, WindowContext, + Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, + ElementInputHandler, Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, Size, Style, TextRun, TextStyle, + ViewContext, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -1176,30 +1177,31 @@ impl EditorElement { } } - // fn paint_blocks( - // &mut self, - // bounds: Bounds, - // visible_bounds: Bounds, - // layout: &mut LayoutState, - // editor: &mut Editor, - // cx: &mut ViewContext, - // ) { - // let scroll_position = layout.position_map.snapshot.scroll_position(); - // let scroll_left = scroll_position.x * layout.position_map.em_width; - // let scroll_top = scroll_position.y * layout.position_map.line_height; + fn paint_blocks( + &mut self, + bounds: Bounds, + layout: &mut LayoutState, + editor: &mut Editor, + cx: &mut ViewContext, + ) { + let scroll_position = layout.position_map.snapshot.scroll_position(); + let scroll_left = scroll_position.x * layout.position_map.em_width; + let scroll_top = scroll_position.y * layout.position_map.line_height; - // for block in &mut layout.blocks { - // let mut origin = bounds.origin - // + point( - // 0., - // block.row as f32 * layout.position_map.line_height - scroll_top, - // ); - // if !matches!(block.style, BlockStyle::Sticky) { - // origin += point(-scroll_left, 0.); - // } - // block.element.paint(origin, visible_bounds, editor, cx); - // } - // } + for block in &mut layout.blocks { + let mut origin = bounds.origin + + point( + Pixels::ZERO, + block.row as f32 * layout.position_map.line_height - scroll_top, + ); + if !matches!(block.style, BlockStyle::Sticky) { + origin += point(-scroll_left, Pixels::ZERO); + } + block + .element + .draw(origin, block.available_space, editor, cx); + } + } fn column_pixels(&self, column: usize, cx: &ViewContext) -> Pixels { let style = &self.style; @@ -1942,7 +1944,7 @@ impl EditorElement { fold_ranges, line_number_layouts, display_hunks, - // blocks, + blocks, selections, context_menu, code_actions_indicator, @@ -1978,7 +1980,11 @@ impl EditorElement { TransformBlock::ExcerptHeader { .. } => false, TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, }); - let mut render_block = |block: &TransformBlock, width: Pixels, block_id: usize| { + let mut render_block = |block: &TransformBlock, + available_space: Size, + block_id: usize, + editor: &mut Editor, + cx: &mut ViewContext| { let mut element = match block { TransformBlock::Custom(block) => { let align_to = block @@ -2031,28 +2037,15 @@ impl EditorElement { let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); // todo!("avoid ElementId collision risk here") - IconButton::new(usize::from(*id), ui::Icon::ArrowUpRight) - .on_click(move |editor, cx| { - if let Some(workspace) = editor - .workspace - .as_ref() - .and_then(|(workspace, _)| workspace.upgrade(cx)) - { - workspace.update(cx, |workspace, cx| { - Editor::jump( - workspace, - jump_path.clone(), - jump_position, - jump_anchor, - cx, - ); - }); - } + let icon_button_id: usize = id.clone().into(); + IconButton::new(icon_button_id, ui::Icon::ArrowUpRight) + .on_click(move |editor: &mut Editor, cx| { + editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); }) .tooltip("Jump to Buffer") // todo!(pass an action as well to show key binding) }); - if *starts_new_buffer { + let element = if *starts_new_buffer { let path = buffer.resolve_file_path(cx, include_root); let mut filename = None; let mut parent_path = None; @@ -2066,40 +2059,34 @@ impl EditorElement { h_stack() .child(filename.unwrap_or_else(|| "untitled".to_string())) .children(parent_path) - .children(jump_icon) - .p_x(gutter_padding) + .children(jump_icon) // .p_x(gutter_padding) } else { let text_style = style.text.clone(); - h_stack() - .child("⋯") - .children(jump_icon) - .p_x(gutter_padding) - .expanded() - .into_any_named("collapsed context") - } + h_stack().child("⋯").children(jump_icon) // .p_x(gutter_padding) + }; + element.render() } }; - // element.layout( - // SizeConstraint { - // min: gpui::Point::::zero(), - // max: point(width, block.height() as f32 * line_height), - // }, - // editor, - // cx, - // ); - element + let size = element.measure(available_space, editor, cx); + (element, size) }; let mut fixed_block_max_width = Pixels::ZERO; let mut blocks = Vec::new(); for (row, block) in fixed_blocks { - let element = render_block(block, f32::INFINITY, block_id); + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(block.height() as f32 * line_height), + ); + let (element, element_size) = + render_block(block, available_space, block_id, editor, cx); block_id += 1; - fixed_block_max_width = fixed_block_max_width.max(element.size().x + em_width); + fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width); blocks.push(BlockLayout { row, element, + available_space, style: BlockStyle::Fixed, }); } @@ -2115,11 +2102,16 @@ impl EditorElement { .max(gutter_width + scroll_width), BlockStyle::Fixed => unreachable!(), }; - let element = render_block(block, width, block_id); + let available_space = size( + AvailableSpace::Definite(width), + AvailableSpace::Definite(block.height() as f32 * line_height), + ); + let (element, _) = render_block(block, available_space, block_id, editor, cx); block_id += 1; blocks.push(BlockLayout { row, element, + available_space, style, }); } @@ -2630,11 +2622,18 @@ impl Element for EditorElement { &layout.position_map, cx, ); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); if layout.gutter_size.width > Pixels::ZERO { self.paint_gutter(gutter_bounds, &mut layout, editor, cx); } + self.paint_text(text_bounds, &mut layout, editor, cx); + + if !layout.blocks.is_empty() { + self.paint_blocks(bounds, &mut layout, editor, cx); + } + let input_handler = ElementInputHandler::new(bounds, cx); cx.handle_input(&editor.focus_handle, input_handler); }); @@ -3255,7 +3254,7 @@ pub struct LayoutState { highlighted_rows: Option>, line_number_layouts: Vec>, display_hunks: Vec, - // blocks: Vec, + blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, fold_ranges: Vec<(BufferRow, Range, Hsla)>, selections: Vec<(PlayerColor, Vec)>, @@ -3358,6 +3357,7 @@ impl PositionMap { struct BlockLayout { row: u32, element: AnyElement, + available_space: Size, style: BlockStyle, } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index e6c0e3f44d..ee3e9708c0 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -9,8 +9,10 @@ pub struct TextTooltip { } impl TextTooltip { - pub fn new(str: SharedString) -> Self { - Self { title: str } + pub fn new(title: impl Into) -> Self { + Self { + title: title.into(), + } } } From b6914bf0fda2e04415db3b2f4d26650e89a3b71d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 16:09:29 +0100 Subject: [PATCH 087/126] Re-enable find all references Co-Authored-By: Julia --- crates/editor2/src/editor.rs | 84 ++++++++++++++++------------------- crates/editor2/src/element.rs | 6 ++- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 22ceea51a3..d02521fac1 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -7588,53 +7588,47 @@ impl Editor { }) } - // pub fn find_all_references( - // workspace: &mut Workspace, - // _: &FindAllReferences, - // cx: &mut ViewContext, - // ) -> Option>> { - // let active_item = workspace.active_item(cx)?; - // let editor_handle = active_item.act_as::(cx)?; + pub fn find_all_references( + &mut self, + _: &FindAllReferences, + cx: &mut ViewContext, + ) -> Option>> { + let buffer = self.buffer.read(cx); + let head = self.selections.newest::(cx).head(); + let (buffer, head) = buffer.text_anchor_for_position(head, cx)?; + let replica_id = self.replica_id(cx); - // let editor = editor_handle.read(cx); - // let buffer = editor.buffer.read(cx); - // let head = editor.selections.newest::(cx).head(); - // let (buffer, head) = buffer.text_anchor_for_position(head, cx)?; - // let replica_id = editor.replica_id(cx); + let workspace = self.workspace()?; + let project = workspace.read(cx).project().clone(); + let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); + Some(cx.spawn(|_, mut cx| async move { + let locations = references.await?; + if locations.is_empty() { + return Ok(()); + } - // let project = workspace.project().clone(); - // let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); - // Some(cx.spawn_labeled( - // "Finding All References...", - // |workspace, mut cx| async move { - // let locations = references.await?; - // if locations.is_empty() { - // return Ok(()); - // } + workspace.update(&mut cx, |workspace, cx| { + let title = locations + .first() + .as_ref() + .map(|location| { + let buffer = location.buffer.read(cx); + format!( + "References to `{}`", + buffer + .text_for_range(location.range.clone()) + .collect::() + ) + }) + .unwrap(); + Self::open_locations_in_multibuffer( + workspace, locations, replica_id, title, false, cx, + ); + })?; - // workspace.update(&mut cx, |workspace, cx| { - // let title = locations - // .first() - // .as_ref() - // .map(|location| { - // let buffer = location.buffer.read(cx); - // format!( - // "References to `{}`", - // buffer - // .text_for_range(location.range.clone()) - // .collect::() - // ) - // }) - // .unwrap(); - // Self::open_locations_in_multibuffer( - // workspace, locations, replica_id, title, false, cx, - // ); - // })?; - - // Ok(()) - // }, - // )) - // } + Ok(()) + })) + } /// Opens a multibuffer with the given project locations in it pub fn open_locations_in_multibuffer( @@ -7685,7 +7679,7 @@ impl Editor { editor.update(cx, |editor, cx| { editor.highlight_background::( ranges_to_highlight, - |theme| todo!("theme.editor.highlighted_line_background"), + |theme| theme.editor_highlighted_line_background, cx, ); }); diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 64a281d9e2..38b54ea2b1 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2565,7 +2565,11 @@ impl Element for EditorElement { }); // on_action(cx, Editor::rename); todo!() // on_action(cx, Editor::confirm_rename); todo!() - // on_action(cx, Editor::find_all_references); todo!() + register_action(cx, |editor, action, cx| { + editor + .find_all_references(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); register_action(cx, Editor::next_copilot_suggestion); register_action(cx, Editor::previous_copilot_suggestion); register_action(cx, Editor::copilot_suggest); From d855e91e438006ef03f8f118a7eec0d0415cc465 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 16:38:20 +0100 Subject: [PATCH 088/126] Honor `cmd-w` to close active item Co-Authored-By: Julia --- crates/gpui2/src/action.rs | 6 +++++- crates/workspace2/src/pane.rs | 35 ++++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 06e93e275d..16487cf18a 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -68,8 +68,12 @@ where A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static, { fn qualified_name() -> SharedString { + let name = type_name::(); + let mut separator_matches = name.rmatch_indices("::"); + separator_matches.next().unwrap(); + let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2); // todo!() remove the 2 replacement when migration is done - type_name::().replace("2::", "::").into() + name[name_start_ix..].replace("2::", "::").into() } fn build(params: Option) -> Result> diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index d0613e13ab..e3ea4863c9 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -733,21 +733,21 @@ impl Pane { // self.activate_item(index, activate_pane, activate_pane, cx); // } - // pub fn close_active_item( - // &mut self, - // action: &CloseActiveItem, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_item_by_id( - // active_item_id, - // action.save_intent.unwrap_or(SaveIntent::Close), - // cx, - // )) - // } + pub fn close_active_item( + &mut self, + action: &CloseActiveItem, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].id(); + Some(self.close_item_by_id( + active_item_id, + action.save_intent.unwrap_or(SaveIntent::Close), + cx, + )) + } pub fn close_item_by_id( &mut self, @@ -1919,7 +1919,12 @@ impl Render for Pane { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { v_stack() + .context("Pane") .size_full() + .on_action(|pane: &mut Self, action, cx| { + pane.close_active_item(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }) .child(self.render_tab_bar(cx)) .child(div() /* todo!(toolbar) */) .child(if let Some(item) = self.active_item() { From 0b8ec5372ba5679f02935c037bffbe623869a825 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 17:06:18 +0100 Subject: [PATCH 089/126] Return the line length when `x` is past the last glyph Co-Authored-By: Julia --- crates/gpui2/src/text_system/line_layout.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index db7140b040..7e9176caca 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/crates/gpui2/src/text_system/line_layout.rs @@ -54,9 +54,9 @@ impl LineLayout { pub fn closest_index_for_x(&self, x: Pixels) -> usize { let mut prev_index = 0; let mut prev_x = px(0.); + for run in self.runs.iter() { for glyph in run.glyphs.iter() { - glyph.index; if glyph.position.x >= x { if glyph.position.x - x < x - prev_x { return glyph.index; @@ -68,7 +68,8 @@ impl LineLayout { prev_x = glyph.position.x; } } - prev_index + 1 + + self.len } pub fn x_for_index(&self, index: usize) -> Pixels { From 7f5014b34a29ec39099f222f125756caf97220e6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 17:15:33 +0100 Subject: [PATCH 090/126] Add red background to blocks that need styling --- crates/editor2/src/editor.rs | 2 ++ crates/editor2/src/element.rs | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index d02521fac1..ebe78d95b3 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9974,6 +9974,8 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend let message = message.clone(); v_stack() .id(cx.block_id) + .size_full() + .bg(gpui::red()) .children(highlighted_lines.iter().map(|(line, highlights)| { div() .child(HighlightedLabel::new(line.clone(), highlights.clone())) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 38b54ea2b1..638ed33891 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -21,7 +21,7 @@ use gpui::{ point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, Size, Style, TextRun, TextStyle, + MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, Size, Style, Styled, TextRun, TextStyle, ViewContext, WindowContext, }; use itertools::Itertools; @@ -2057,12 +2057,18 @@ impl EditorElement { } h_stack() + .size_full() + .bg(gpui::red()) .child(filename.unwrap_or_else(|| "untitled".to_string())) .children(parent_path) .children(jump_icon) // .p_x(gutter_padding) } else { let text_style = style.text.clone(); - h_stack().child("⋯").children(jump_icon) // .p_x(gutter_padding) + h_stack() + .size_full() + .bg(gpui::red()) + .child("⋯") + .children(jump_icon) // .p_x(gutter_padding) }; element.render() } From 7d94d8940c69a016ff8cb19ed7665647864aa8ee Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 09:28:18 -0700 Subject: [PATCH 091/126] Not working yet file-finder2 --- Cargo.lock | 25 + crates/file_finder2/Cargo.toml | 36 + crates/file_finder2/src/file_finder.rs | 2172 ++++++++++++++++++++++++ crates/workspace2/src/modal_layer.rs | 8 + crates/workspace2/src/workspace2.rs | 4 + crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 2 +- 7 files changed, 2247 insertions(+), 2 deletions(-) create mode 100644 crates/file_finder2/Cargo.toml create mode 100644 crates/file_finder2/src/file_finder.rs diff --git a/Cargo.lock b/Cargo.lock index 0882435df9..bbc88e6785 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3061,6 +3061,30 @@ dependencies = [ "workspace", ] +[[package]] +name = "file_finder2" +version = "0.1.0" +dependencies = [ + "collections", + "ctor", + "editor2", + "env_logger 0.9.3", + "fuzzy2", + "gpui2", + "language2", + "menu2", + "picker2", + "postage", + "project2", + "serde", + "serde_json", + "settings2", + "text2", + "theme2", + "util", + "workspace2", +] + [[package]] name = "filetime" version = "0.2.22" @@ -11393,6 +11417,7 @@ dependencies = [ "editor2", "env_logger 0.9.3", "feature_flags2", + "file_finder2", "fs2", "fsevent", "futures 0.3.28", diff --git a/crates/file_finder2/Cargo.toml b/crates/file_finder2/Cargo.toml new file mode 100644 index 0000000000..8950cff792 --- /dev/null +++ b/crates/file_finder2/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "file_finder2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/file_finder.rs" +doctest = false + +[dependencies] +editor = { package = "editor2", path = "../editor2" } +collections = { path = "../collections" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +menu = { package = "menu2", path = "../menu2" } +picker = { package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +text = { package = "text2", path = "../text2" } +util = { path = "../util" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2" } +postage.workspace = true +serde.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } +theme = { package = "theme2", path = "../theme2", features = ["test-support"] } + +serde_json.workspace = true +ctor.workspace = true +env_logger.workspace = true diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs new file mode 100644 index 0000000000..a9b5be1dcd --- /dev/null +++ b/crates/file_finder2/src/file_finder.rs @@ -0,0 +1,2172 @@ +use collections::HashMap; +use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; +use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; +use gpui::{actions, AppContext, Task, ViewContext, View, EventEmitter, WindowContext}; +use picker::{Picker, PickerDelegate}; +use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; +use std::{ + path::{Path, PathBuf}, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, +}; +use text::Point; +use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; +use workspace::{Workspace, Modal, ModalEvent}; + +actions!(Toggle); + +pub struct FileFinder { + picker: View> +} + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(FileFinder::register); +} + +impl FileFinder { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &Toggle, cx| { + workspace.toggle_modal(cx, |cx| FileFinder::new(cx)); + }); + } + + fn new(cx: &mut ViewContext) -> Self { + FileFinder{ + + } + } +} + +impl EventEmitter for FileFinder; +impl Modal for FileFinder{ + fn focus(&self, cx: &mut WindowContext) { + self.picker.update(cx, |picker, cx| { picker.focus(cx) }) + } +} + +pub struct FileFinderDelegate { + workspace: WeakViewHandle, + project: ModelHandle, + search_count: usize, + latest_search_id: usize, + latest_search_did_cancel: bool, + latest_search_query: Option>, + currently_opened_path: Option, + matches: Matches, + selected_index: Option, + cancel_flag: Arc, + history_items: Vec, +} + +#[derive(Debug, Default)] +struct Matches { + history: Vec<(FoundPath, Option)>, + search: Vec, +} + +#[derive(Debug)] +enum Match<'a> { + History(&'a FoundPath, Option<&'a PathMatch>), + Search(&'a PathMatch), +} + +impl Matches { + fn len(&self) -> usize { + self.history.len() + self.search.len() + } + + fn get(&self, index: usize) -> Option> { + if index < self.history.len() { + self.history + .get(index) + .map(|(path, path_match)| Match::History(path, path_match.as_ref())) + } else { + self.search + .get(index - self.history.len()) + .map(Match::Search) + } + } + + fn push_new_matches( + &mut self, + history_items: &Vec, + query: &PathLikeWithPosition, + mut new_search_matches: Vec, + extend_old_matches: bool, + ) { + let matching_history_paths = matching_history_item_paths(history_items, query); + new_search_matches + .retain(|path_match| !matching_history_paths.contains_key(&path_match.path)); + let history_items_to_show = history_items + .iter() + .filter_map(|history_item| { + Some(( + history_item.clone(), + Some( + matching_history_paths + .get(&history_item.project.path)? + .clone(), + ), + )) + }) + .collect::>(); + self.history = history_items_to_show; + if extend_old_matches { + self.search + .retain(|path_match| !matching_history_paths.contains_key(&path_match.path)); + util::extend_sorted( + &mut self.search, + new_search_matches.into_iter(), + 100, + |a, b| b.cmp(a), + ) + } else { + self.search = new_search_matches; + } + } +} + +fn matching_history_item_paths( + history_items: &Vec, + query: &PathLikeWithPosition, +) -> HashMap, PathMatch> { + let history_items_by_worktrees = history_items + .iter() + .filter_map(|found_path| { + let candidate = PathMatchCandidate { + path: &found_path.project.path, + // Only match history items names, otherwise their paths may match too many queries, producing false positives. + // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item, + // it would be shown first always, despite the latter being a better match. + char_bag: CharBag::from_iter( + found_path + .project + .path + .file_name()? + .to_string_lossy() + .to_lowercase() + .chars(), + ), + }; + Some((found_path.project.worktree_id, candidate)) + }) + .fold( + HashMap::default(), + |mut candidates, (worktree_id, new_candidate)| { + candidates + .entry(worktree_id) + .or_insert_with(Vec::new) + .push(new_candidate); + candidates + }, + ); + let mut matching_history_paths = HashMap::default(); + for (worktree, candidates) in history_items_by_worktrees { + let max_results = candidates.len() + 1; + matching_history_paths.extend( + fuzzy::match_fixed_path_set( + candidates, + worktree.to_usize(), + query.path_like.path_query(), + false, + max_results, + ) + .into_iter() + .map(|path_match| (Arc::clone(&path_match.path), path_match)), + ); + } + matching_history_paths +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct FoundPath { + project: ProjectPath, + absolute: Option, +} + +impl FoundPath { + fn new(project: ProjectPath, absolute: Option) -> Self { + Self { project, absolute } + } +} + +const MAX_RECENT_SELECTIONS: usize = 20; + +fn toggle_or_cycle_file_finder( + workspace: &mut Workspace, + _: &Toggle, + cx: &mut ViewContext, +) { + match workspace.modal::() { + Some(file_finder) => file_finder.update(cx, |file_finder, cx| { + let current_index = file_finder.delegate().selected_index(); + file_finder.select_next(&menu::SelectNext, cx); + let new_index = file_finder.delegate().selected_index(); + if current_index == new_index { + file_finder.select_first(&menu::SelectFirst, cx); + } + }), + None => { + workspace.toggle_modal(cx, |workspace, cx| { + let project = workspace.project().read(cx); + + let currently_opened_path = workspace + .active_item(cx) + .and_then(|item| item.project_path(cx)) + .map(|project_path| { + let abs_path = project + .worktree_for_id(project_path.worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)); + FoundPath::new(project_path, abs_path) + }); + + // if exists, bubble the currently opened path to the top + let history_items = currently_opened_path + .clone() + .into_iter() + .chain( + workspace + .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx) + .into_iter() + .filter(|(history_path, _)| { + Some(history_path) + != currently_opened_path + .as_ref() + .map(|found_path| &found_path.project) + }) + .filter(|(_, history_abs_path)| { + history_abs_path.as_ref() + != currently_opened_path + .as_ref() + .and_then(|found_path| found_path.absolute.as_ref()) + }) + .filter(|(_, history_abs_path)| match history_abs_path { + Some(abs_path) => history_file_exists(abs_path), + None => true, + }) + .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)), + ) + .collect(); + + let project = workspace.project().clone(); + let workspace = cx.handle().downgrade(); + let finder = cx.add_view(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace, + project, + currently_opened_path, + history_items, + cx, + ), + cx, + ) + }); + finder + }); + } + } +} + +#[cfg(not(test))] +fn history_file_exists(abs_path: &PathBuf) -> bool { + abs_path.exists() +} + +#[cfg(test)] +fn history_file_exists(abs_path: &PathBuf) -> bool { + !abs_path.ends_with("nonexistent.rs") +} + +pub enum Event { + Selected(ProjectPath), + Dismissed, +} + +#[derive(Debug, Clone)] +struct FileSearchQuery { + raw_query: String, + file_query_end: Option, +} + +impl FileSearchQuery { + fn path_query(&self) -> &str { + match self.file_query_end { + Some(file_path_end) => &self.raw_query[..file_path_end], + None => &self.raw_query, + } + } +} + +impl FileFinderDelegate { + fn new( + workspace: WeakViewHandle, + project: ModelHandle, + currently_opened_path: Option, + history_items: Vec, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&project, |picker, _, cx| { + picker.update_matches(picker.query(cx), cx); + }) + .detach(); + Self { + workspace, + project, + search_count: 0, + latest_search_id: 0, + latest_search_did_cancel: false, + latest_search_query: None, + currently_opened_path, + matches: Matches::default(), + selected_index: None, + cancel_flag: Arc::new(AtomicBool::new(false)), + history_items, + } + } + + fn spawn_search( + &mut self, + query: PathLikeWithPosition, + cx: &mut ViewContext, + ) -> Task<()> { + let relative_to = self + .currently_opened_path + .as_ref() + .map(|found_path| Arc::clone(&found_path.project.path)); + let worktrees = self + .project + .read(cx) + .visible_worktrees(cx) + .collect::>(); + let include_root_name = worktrees.len() > 1; + let candidate_sets = worktrees + .into_iter() + .map(|worktree| { + let worktree = worktree.read(cx); + PathMatchCandidateSet { + snapshot: worktree.snapshot(), + include_ignored: worktree + .root_entry() + .map_or(false, |entry| entry.is_ignored), + include_root_name, + } + }) + .collect::>(); + + let search_id = util::post_inc(&mut self.search_count); + self.cancel_flag.store(true, atomic::Ordering::Relaxed); + self.cancel_flag = Arc::new(AtomicBool::new(false)); + let cancel_flag = self.cancel_flag.clone(); + cx.spawn(|picker, mut cx| async move { + let matches = fuzzy::match_path_sets( + candidate_sets.as_slice(), + query.path_like.path_query(), + relative_to, + false, + 100, + &cancel_flag, + cx.background(), + ) + .await; + let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); + picker + .update(&mut cx, |picker, cx| { + picker + .delegate_mut() + .set_search_matches(search_id, did_cancel, query, matches, cx) + }) + .log_err(); + }) + } + + fn set_search_matches( + &mut self, + search_id: usize, + did_cancel: bool, + query: PathLikeWithPosition, + matches: Vec, + cx: &mut ViewContext, + ) { + if search_id >= self.latest_search_id { + self.latest_search_id = search_id; + let extend_old_matches = self.latest_search_did_cancel + && Some(query.path_like.path_query()) + == self + .latest_search_query + .as_ref() + .map(|query| query.path_like.path_query()); + self.matches + .push_new_matches(&self.history_items, &query, matches, extend_old_matches); + self.latest_search_query = Some(query); + self.latest_search_did_cancel = did_cancel; + cx.notify(); + } + } + + fn labels_for_match( + &self, + path_match: Match, + cx: &AppContext, + ix: usize, + ) -> (String, Vec, String, Vec) { + let (file_name, file_name_positions, full_path, full_path_positions) = match path_match { + Match::History(found_path, found_path_match) => { + let worktree_id = found_path.project.worktree_id; + let project_relative_path = &found_path.project.path; + let has_worktree = self + .project + .read(cx) + .worktree_for_id(worktree_id, cx) + .is_some(); + + if !has_worktree { + if let Some(absolute_path) = &found_path.absolute { + return ( + absolute_path + .file_name() + .map_or_else( + || project_relative_path.to_string_lossy(), + |file_name| file_name.to_string_lossy(), + ) + .to_string(), + Vec::new(), + absolute_path.to_string_lossy().to_string(), + Vec::new(), + ); + } + } + + let mut path = Arc::clone(project_relative_path); + if project_relative_path.as_ref() == Path::new("") { + if let Some(absolute_path) = &found_path.absolute { + path = Arc::from(absolute_path.as_path()); + } + } + + let mut path_match = PathMatch { + score: ix as f64, + positions: Vec::new(), + worktree_id: worktree_id.to_usize(), + path, + path_prefix: "".into(), + distance_to_relative_ancestor: usize::MAX, + }; + if let Some(found_path_match) = found_path_match { + path_match + .positions + .extend(found_path_match.positions.iter()) + } + + self.labels_for_path_match(&path_match) + } + Match::Search(path_match) => self.labels_for_path_match(path_match), + }; + + if file_name_positions.is_empty() { + if let Some(user_home_path) = std::env::var("HOME").ok() { + let user_home_path = user_home_path.trim(); + if !user_home_path.is_empty() { + if (&full_path).starts_with(user_home_path) { + return ( + file_name, + file_name_positions, + full_path.replace(user_home_path, "~"), + full_path_positions, + ); + } + } + } + } + + ( + file_name, + file_name_positions, + full_path, + full_path_positions, + ) + } + + fn labels_for_path_match( + &self, + path_match: &PathMatch, + ) -> (String, Vec, String, Vec) { + let path = &path_match.path; + let path_string = path.to_string_lossy(); + let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join(""); + let path_positions = path_match.positions.clone(); + + let file_name = path.file_name().map_or_else( + || path_match.path_prefix.to_string(), + |file_name| file_name.to_string_lossy().to_string(), + ); + let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count() + - file_name.chars().count(); + let file_name_positions = path_positions + .iter() + .filter_map(|pos| { + if pos >= &file_name_start { + Some(pos - file_name_start) + } else { + None + } + }) + .collect(); + + (file_name, file_name_positions, full_path, path_positions) + } +} + +impl PickerDelegate for FileFinderDelegate { + fn placeholder_text(&self) -> Arc { + "Search project files...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index.unwrap_or(0) + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + self.selected_index = Some(ix); + cx.notify(); + } + + fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext) -> Task<()> { + if raw_query.is_empty() { + let project = self.project.read(cx); + self.latest_search_id = post_inc(&mut self.search_count); + self.matches = Matches { + history: self + .history_items + .iter() + .filter(|history_item| { + project + .worktree_for_id(history_item.project.worktree_id, cx) + .is_some() + || (project.is_local() && history_item.absolute.is_some()) + }) + .cloned() + .map(|p| (p, None)) + .collect(), + search: Vec::new(), + }; + cx.notify(); + Task::ready(()) + } else { + let raw_query = &raw_query; + let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: raw_query.to_owned(), + file_query_end: if path_like_str == raw_query { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .expect("infallible"); + self.spawn_search(query, cx) + } + } + + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext) { + if let Some(m) = self.matches.get(self.selected_index()) { + if let Some(workspace) = self.workspace.upgrade(cx) { + let open_task = workspace.update(cx, move |workspace, cx| { + let split_or_open = |workspace: &mut Workspace, project_path, cx| { + if secondary { + workspace.split_path(project_path, cx) + } else { + workspace.open_path(project_path, None, true, cx) + } + }; + match m { + Match::History(history_match, _) => { + let worktree_id = history_match.project.worktree_id; + if workspace + .project() + .read(cx) + .worktree_for_id(worktree_id, cx) + .is_some() + { + split_or_open( + workspace, + ProjectPath { + worktree_id, + path: Arc::clone(&history_match.project.path), + }, + cx, + ) + } else { + match history_match.absolute.as_ref() { + Some(abs_path) => { + if secondary { + workspace.split_abs_path( + abs_path.to_path_buf(), + false, + cx, + ) + } else { + workspace.open_abs_path( + abs_path.to_path_buf(), + false, + cx, + ) + } + } + None => split_or_open( + workspace, + ProjectPath { + worktree_id, + path: Arc::clone(&history_match.project.path), + }, + cx, + ), + } + } + } + Match::Search(m) => split_or_open( + workspace, + ProjectPath { + worktree_id: WorktreeId::from_usize(m.worktree_id), + path: m.path.clone(), + }, + cx, + ), + } + }); + + let row = self + .latest_search_query + .as_ref() + .and_then(|query| query.row) + .map(|row| row.saturating_sub(1)); + let col = self + .latest_search_query + .as_ref() + .and_then(|query| query.column) + .unwrap_or(0) + .saturating_sub(1); + cx.spawn(|_, mut cx| async move { + let item = open_task.await.log_err()?; + if let Some(row) = row { + if let Some(active_editor) = item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; + let point = snapshot + .buffer_snapshot + .clip_point(Point::new(row, col), Bias::Left); + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([point..point]) + }); + }) + .log_err(); + } + } + workspace + .downgrade() + .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx)) + .log_err(); + + Some(()) + }) + .detach(); + } + } + } + + fn dismissed(&mut self, _: &mut ViewContext) {} + + fn render_match( + &self, + ix: usize, + mouse_state: &mut MouseState, + selected: bool, + cx: &AppContext, + ) -> AnyElement> { + let path_match = self + .matches + .get(ix) + .expect("Invalid matches state: no element for index {ix}"); + let theme = theme::current(cx); + let style = theme.picker.item.in_state(selected).style_for(mouse_state); + let (file_name, file_name_positions, full_path, full_path_positions) = + self.labels_for_match(path_match, cx, ix); + Flex::column() + .with_child( + Label::new(file_name, style.label.clone()).with_highlights(file_name_positions), + ) + .with_child( + Label::new(full_path, style.label.clone()).with_highlights(full_path_positions), + ) + .flex(1., false) + .contained() + .with_style(style.container) + .into_any_named("match") + } +} + +#[cfg(test)] +mod tests { + use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; + + use super::*; + use editor::Editor; + use gpui::{TestAppContext, ViewHandle}; + use menu::{Confirm, SelectNext}; + use serde_json::json; + use workspace::{AppState, Workspace}; + + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } + + #[gpui::test] + async fn test_matching_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "banana": "", + "bandana": "", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + cx.dispatch_action(window.into(), Toggle); + + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder.delegate_mut().update_matches("bna".to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + assert_eq!(finder.delegate().matches.len(), 2); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window.into(), SelectNext); + cx.dispatch_action(window.into(), Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + cx.read(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + assert_eq!( + active_item + .as_any() + .downcast_ref::() + .unwrap() + .read(cx) + .title(cx), + "bandana" + ); + }); + } + + #[gpui::test] + async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + cx.dispatch_action(window.into(), Toggle); + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + + let file_query = &first_file_name[..3]; + let file_row = 1; + let file_column = 3; + assert!(file_column <= first_file_contents.len()); + let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(query_inside_file.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let finder = finder.delegate(); + assert_eq!(finder.matches.len(), 1); + let latest_search_query = finder + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window.into(), SelectNext); + cx.dispatch_action(window.into(), Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + let editor = cx.update(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + active_item.downcast::().unwrap() + }); + cx.foreground().advance_clock(Duration::from_secs(2)); + cx.foreground().start_waiting(); + cx.foreground().finish_waiting(); + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(file_row, caret_selection.start.row + 1, + "Query inside file should get caret with the same focus row"); + assert_eq!(file_column, caret_selection.start.column as usize + 1, + "Query inside file should get caret with the same focus column"); + }); + } + + #[gpui::test] + async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + cx.dispatch_action(window.into(), Toggle); + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + + let file_query = &first_file_name[..3]; + let file_row = 200; + let file_column = 300; + assert!(file_column > first_file_contents.len()); + let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(query_outside_file.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let finder = finder.delegate(); + assert_eq!(finder.matches.len(), 1); + let latest_search_query = finder + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window.into(), SelectNext); + cx.dispatch_action(window.into(), Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + let editor = cx.update(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + active_item.downcast::().unwrap() + }); + cx.foreground().advance_clock(Duration::from_secs(2)); + cx.foreground().start_waiting(); + cx.foreground().finish_waiting(); + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(0, caret_selection.start.row, + "Excessive rows (as in query outside file borders) should get trimmed to last file row"); + assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, + "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); + }); + } + + #[gpui::test] + async fn test_matching_cancellation(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/dir", + json!({ + "hello": "", + "goodbye": "", + "halogen-light": "", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .root(cx); + let finder = cx + .add_window(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + None, + Vec::new(), + cx, + ), + cx, + ) + }) + .root(cx); + + let query = test_path_like("hi"); + finder + .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) + .await; + finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5)); + + finder.update(cx, |finder, cx| { + let delegate = finder.delegate_mut(); + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); + + // Simulate a search being cancelled after the time limit, + // returning only a subset of the matches that would have been found. + drop(delegate.spawn_search(query.clone(), cx)); + delegate.set_search_matches( + delegate.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[1].clone(), matches[3].clone()], + cx, + ); + + // Simulate another cancellation. + drop(delegate.spawn_search(query.clone(), cx)); + delegate.set_search_matches( + delegate.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], + cx, + ); + + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); + }); + } + + #[gpui::test] + async fn test_ignored_files(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/ancestor", + json!({ + ".gitignore": "ignored-root", + "ignored-root": { + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + "tracked-root": { + ".gitignore": "height", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + }), + ) + .await; + + let project = Project::test( + app_state.fs.clone(), + [ + "/ancestor/tracked-root".as_ref(), + "/ancestor/ignored-root".as_ref(), + ], + cx, + ) + .await; + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .root(cx); + let finder = cx + .add_window(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + None, + Vec::new(), + cx, + ), + cx, + ) + }) + .root(cx); + finder + .update(cx, |f, cx| { + f.delegate_mut().spawn_search(test_path_like("hi"), cx) + }) + .await; + finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); + } + + #[gpui::test] + async fn test_single_file_worktrees(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) + .await; + + let project = Project::test( + app_state.fs.clone(), + ["/root/the-parent-dir/the-file".as_ref()], + cx, + ) + .await; + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .root(cx); + let finder = cx + .add_window(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + None, + Vec::new(), + cx, + ), + cx, + ) + }) + .root(cx); + + // Even though there is only one worktree, that worktree's filename + // is included in the matching, because the worktree is a single file. + finder + .update(cx, |f, cx| { + f.delegate_mut().spawn_search(test_path_like("thf"), cx) + }) + .await; + cx.read(|cx| { + let finder = finder.read(cx); + let delegate = finder.delegate(); + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); + assert_eq!(matches.len(), 1); + + let (file_name, file_name_positions, full_path, full_path_positions) = + delegate.labels_for_path_match(&matches[0]); + assert_eq!(file_name, "the-file"); + assert_eq!(file_name_positions, &[0, 1, 4]); + assert_eq!(full_path, "the-file"); + assert_eq!(full_path_positions, &[0, 1, 4]); + }); + + // Since the worktree root is a file, searching for its name followed by a slash does + // not match anything. + finder + .update(cx, |f, cx| { + f.delegate_mut().spawn_search(test_path_like("thf/"), cx) + }) + .await; + finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); + } + + #[gpui::test] + async fn test_path_distance_ordering(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "dir1": { "a.txt": "" }, + "dir2": { + "a.txt": "", + "b.txt": "" + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .root(cx); + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + WorktreeId::from_usize(worktrees[0].id()) + }); + + // When workspace has an active item, sort items which are closer to that item + // first when they have the same name. In this case, b.txt is closer to dir2's a.txt + // so that one should be sorted earlier + let b_path = Some(dummy_found_path(ProjectPath { + worktree_id, + path: Arc::from(Path::new("/root/dir2/b.txt")), + })); + let finder = cx + .add_window(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + b_path, + Vec::new(), + cx, + ), + cx, + ) + }) + .root(cx); + + finder + .update(cx, |f, cx| { + f.delegate_mut().spawn_search(test_path_like("a.txt"), cx) + }) + .await; + + finder.read_with(cx, |f, _| { + let delegate = f.delegate(); + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); + assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); + assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); + }); + } + + #[gpui::test] + async fn test_search_worktree_without_files(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "dir1": {}, + "dir2": { + "dir3": {} + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .root(cx); + let finder = cx + .add_window(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + None, + Vec::new(), + cx, + ), + cx, + ) + }) + .root(cx); + finder + .update(cx, |f, cx| { + f.delegate_mut().spawn_search(test_path_like("dir"), cx) + }) + .await; + cx.read(|cx| { + let finder = finder.read(cx); + assert_eq!(finder.delegate().matches.len(), 0); + }); + } + + #[gpui::test] + async fn test_query_history( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + WorktreeId::from_usize(worktrees[0].id()) + }); + + // Open and close panels, getting their history items afterwards. + // Ensure history items get populated with opened items, and items are kept in a certain order. + // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. + // + // TODO: without closing, the opened items do not propagate their history changes for some reason + // it does work in real app though, only tests do not propagate. + + let initial_history = open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + assert!( + initial_history.is_empty(), + "Should have no history before opening any files" + ); + + let history_after_first = open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_first, + vec![FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + )], + "Should show 1st opened item in the history when opening the 2nd item" + ); + + let history_after_second = open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_second, + vec![ + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + Some(PathBuf::from("/src/test/second.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + ), + ], + "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ +2nd item should be the first in the history, as the last opened." + ); + + let history_after_third = open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_third, + vec![ + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/third.rs")), + }, + Some(PathBuf::from("/src/test/third.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + Some(PathBuf::from("/src/test/second.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + ), + ], + "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ +3rd item should be the first in the history, as the last opened." + ); + + let history_after_second_again = open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_second_again, + vec![ + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + Some(PathBuf::from("/src/test/second.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/third.rs")), + }, + Some(PathBuf::from("/src/test/third.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + ), + ], + "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ +2nd item, as the last opened, 3rd item should go next as it was opened right before." + ); + } + + #[gpui::test] + async fn test_external_files_history( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + app_state + .fs + .as_fake() + .insert_tree( + "/external-src", + json!({ + "test": { + "third.rs": "// Third Rust file", + "fourth.rs": "// Fourth Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + cx.update(|cx| { + project.update(cx, |project, cx| { + project.find_or_create_local_worktree("/external-src", false, cx) + }) + }) + .detach(); + deterministic.run_until_parked(); + + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1,); + + WorktreeId::from_usize(worktrees[0].id()) + }); + workspace + .update(cx, |workspace, cx| { + workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) + }) + .detach(); + deterministic.run_until_parked(); + let external_worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!( + worktrees.len(), + 2, + "External file should get opened in a new worktree" + ); + + WorktreeId::from_usize( + worktrees + .into_iter() + .find(|worktree| worktree.id() != worktree_id.to_usize()) + .expect("New worktree should have a different id") + .id(), + ) + }); + close_active_item(&workspace, &deterministic, cx).await; + + let initial_history_items = open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + initial_history_items, + vec![FoundPath::new( + ProjectPath { + worktree_id: external_worktree_id, + path: Arc::from(Path::new("")), + }, + Some(PathBuf::from("/external-src/test/third.rs")) + )], + "Should show external file with its full path in the history after it was open" + ); + + let updated_history_items = open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + updated_history_items, + vec![ + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + Some(PathBuf::from("/src/test/second.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id: external_worktree_id, + path: Arc::from(Path::new("")), + }, + Some(PathBuf::from("/external-src/test/third.rs")) + ), + ], + "Should keep external file with history updates", + ); + } + + #[gpui::test] + async fn test_toggle_panel_new_selections( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + + // generate some history to select from + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + let current_history = open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + for expected_selected_index in 0..current_history.len() { + cx.dispatch_action(window.into(), Toggle); + let selected_index = cx.read(|cx| { + workspace + .read(cx) + .modal::() + .unwrap() + .read(cx) + .delegate() + .selected_index() + }); + assert_eq!( + selected_index, expected_selected_index, + "Should select the next item in the history" + ); + } + + cx.dispatch_action(window.into(), Toggle); + let selected_index = cx.read(|cx| { + workspace + .read(cx) + .modal::() + .unwrap() + .read(cx) + .delegate() + .selected_index() + }); + assert_eq!( + selected_index, 0, + "Should wrap around the history and start all over" + ); + } + + #[gpui::test] + async fn test_search_preserves_history_items( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + "fourth.rs": "// Fourth Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1,); + + WorktreeId::from_usize(worktrees[0].id()) + }); + + // generate some history to select from + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + cx.dispatch_action(window.into(), Toggle); + let first_query = "f"; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(first_query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); + let history_match = delegate.matches.history.first().unwrap(); + assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); + assert_eq!(history_match.0, FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + )); + assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); + assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); + }); + + let second_query = "fsdasdsa"; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(second_query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + assert!( + delegate.matches.history.is_empty(), + "No history entries should match {second_query}" + ); + assert!( + delegate.matches.search.is_empty(), + "No search entries should match {second_query}" + ); + }); + + let first_query_again = first_query; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(first_query_again.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); + let history_match = delegate.matches.history.first().unwrap(); + assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); + assert_eq!(history_match.0, FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + )); + assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); + assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); + }); + } + + #[gpui::test] + async fn test_history_items_vs_very_good_external_match( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "collab_ui": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + "collab_ui.rs": "// Fourth Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + // generate some history to select from + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + cx.dispatch_action(window.into(), Toggle); + let query = "collab_ui"; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder.delegate_mut().update_matches(query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + assert!( + delegate.matches.history.is_empty(), + "History items should not math query {query}, they should be matched by name only" + ); + + let search_entries = delegate + .matches + .search + .iter() + .map(|path_match| path_match.path.to_path_buf()) + .collect::>(); + assert_eq!( + search_entries, + vec![ + PathBuf::from("collab_ui/collab_ui.rs"), + PathBuf::from("collab_ui/third.rs"), + PathBuf::from("collab_ui/first.rs"), + PathBuf::from("collab_ui/second.rs"), + ], + "Despite all search results having the same directory name, the most matching one should be on top" + ); + }); + } + + #[gpui::test] + async fn test_nonexistent_history_items_not_shown( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "nonexistent.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + // generate some history to select from + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "non", + 1, + "nonexistent.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + cx.dispatch_action(window.into(), Toggle); + let query = "rs"; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder.delegate_mut().update_matches(query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + let history_entries = delegate + .matches + .history + .iter() + .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) + .collect::>(); + assert_eq!( + history_entries, + vec![ + PathBuf::from("test/first.rs"), + PathBuf::from("test/third.rs"), + ], + "Should have all opened files in the history, except the ones that do not exist on disk" + ); + }); + } + + async fn open_close_queried_buffer( + input: &str, + expected_matches: usize, + expected_editor_title: &str, + window: gpui::AnyWindowHandle, + workspace: &ViewHandle, + deterministic: &gpui::executor::Deterministic, + cx: &mut gpui::TestAppContext, + ) -> Vec { + cx.dispatch_action(window, Toggle); + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder.delegate_mut().update_matches(input.to_string(), cx) + }) + .await; + let history_items = finder.read_with(cx, |finder, _| { + assert_eq!( + finder.delegate().matches.len(), + expected_matches, + "Unexpected number of matches found for query {input}" + ); + finder.delegate().history_items.clone() + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window, SelectNext); + cx.dispatch_action(window, Confirm); + deterministic.run_until_parked(); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + cx.read(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + let active_editor_title = active_item + .as_any() + .downcast_ref::() + .unwrap() + .read(cx) + .title(cx); + assert_eq!( + expected_editor_title, active_editor_title, + "Unexpected editor title for query {input}" + ); + }); + + close_active_item(workspace, deterministic, cx).await; + + history_items + } + + async fn close_active_item( + workspace: &ViewHandle, + deterministic: &gpui::executor::Deterministic, + cx: &mut TestAppContext, + ) { + let mut original_items = HashMap::new(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.id(); + let pane = pane.read(cx); + let insertion_result = original_items.insert(pane_id, pane.items().count()); + assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); + } + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + active_pane + .update(cx, |pane, cx| { + pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) + .unwrap() + }) + .await + .unwrap(); + deterministic.run_until_parked(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.id(); + let pane = pane.read(cx); + match original_items.remove(&pane_id) { + Some(original_items) => { + assert_eq!( + pane.items().count(), + original_items.saturating_sub(1), + "Pane id {pane_id} should have item closed" + ); + } + None => panic!("Pane id {pane_id} not found in original items"), + } + } + }); + assert!( + original_items.len() <= 1, + "At most one panel should got closed" + ); + } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.foreground().forbid_parking(); + cx.update(|cx| { + let state = AppState::test(cx); + theme::init((), cx); + language::init(cx); + super::init(cx); + editor::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + state + }) + } + + fn test_path_like(test_str: &str) -> PathLikeWithPosition { + PathLikeWithPosition::parse_str(test_str, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: test_str.to_owned(), + file_query_end: if path_like_str == test_str { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .unwrap() + } + + fn dummy_found_path(project_path: ProjectPath) -> FoundPath { + FoundPath { + project: project_path, + absolute: None, + } + } +} diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index b3a5de8fb2..bda93a32b9 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -71,6 +71,14 @@ impl ModalLayer { cx.notify(); } + + pub fn current_modal(&self) -> Option> + where + V: 'static, + { + let active_modal = self.active_modal.as_ref()?; + active_modal.modal.clone().downcast::().ok() + } } impl Render for ModalLayer { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 575ab6b8bd..4ee136f47a 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3541,6 +3541,10 @@ impl Workspace { div } + pub fn current_modal(&mut self, cx: &ViewContext) -> Option> { + self.modal_layer.read(cx).current_modal() + } + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) where B: FnOnce(&mut ViewContext) -> V, diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 570912abc5..1b4d5b7196 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -36,7 +36,7 @@ copilot = { package = "copilot2", path = "../copilot2" } db = { package = "db2", path = "../db2" } editor = { package="editor2", path = "../editor2" } # feedback = { path = "../feedback" } -# file_finder = { path = "../file_finder" } +file_finder = { package="file_finder2", path = "../file_finder2" } # search = { path = "../search" } fs = { package = "fs2", path = "../fs2" } fsevent = { path = "../fsevent" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 2deaff2149..a7b1eb02ec 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -186,7 +186,7 @@ fn main() { // recent_projects::init(cx); go_to_line::init(cx); - // file_finder::init(cx); + file_finder::init(cx); // outline::init(cx); // project_symbols::init(cx); // project_panel::init(Assets, cx); From e08f1690b378d43dfd57df4152777a8a431dd1d2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 09:33:28 -0700 Subject: [PATCH 092/126] Remove commented field --- crates/gpui2/src/elements/div.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index aeda28256a..aa4ef9bd36 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -716,7 +716,6 @@ pub struct Interactivity { pub focusable: bool, pub tracked_focus_handle: Option, pub focus_listeners: FocusListeners, - // pub scroll_offset: Point, pub group: Option, pub base_style: StyleRefinement, pub focus_style: StyleRefinement, From 2c3c238c9d3974e2d95ebcc8545d6ba60fd645ed Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Nov 2023 09:47:48 -0800 Subject: [PATCH 093/126] Fix warnings --- crates/project_panel2/src/project_panel.rs | 246 ++++++++++----------- 1 file changed, 119 insertions(+), 127 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index b2ffc0d332..1feead1a19 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -9,8 +9,8 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, - ClipboardItem, Component, Div, Entity, EventEmitter, FocusHandle, FocusableKeyDispatch, Model, - ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, + ClipboardItem, Component, Div, EventEmitter, FocusHandle, FocusableKeyDispatch, Model, + MouseButton, ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; @@ -54,8 +54,8 @@ pub struct ProjectPanel { edit_state: Option, filename_editor: View, clipboard_entry: Option, - dragged_entry_destination: Option>, - workspace: WeakView, + _dragged_entry_destination: Option>, + _workspace: WeakView, has_focus: bool, width: Option, pending_serialization: Task>, @@ -219,7 +219,6 @@ impl ProjectPanel { // }) // .detach(); - let view_id = cx.view().entity_id(); let mut this = Self { project: project.clone(), fs: workspace.app_state().fs.clone(), @@ -233,8 +232,8 @@ impl ProjectPanel { filename_editor, clipboard_entry: None, // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), - dragged_entry_destination: None, - workspace: workspace.weak_handle(), + _dragged_entry_destination: None, + _workspace: workspace.weak_handle(), has_focus: false, width: None, pending_serialization: Task::ready(None), @@ -286,19 +285,19 @@ impl ProjectPanel { } } &Event::SplitEntry { entry_id } => { - // if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - // if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { - // workspace - // .split_path( - // ProjectPath { - // worktree_id: worktree.read(cx).id(), - // path: entry.path.clone(), - // }, - // cx, - // ) - // .detach_and_log_err(cx); - // } - // } + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { + if let Some(_entry) = worktree.read(cx).entry_for_id(entry_id) { + // workspace + // .split_path( + // ProjectPath { + // worktree_id: worktree.read(cx).id(), + // path: entry.path.clone(), + // }, + // cx, + // ) + // .detach_and_log_err(cx); + } + } } _ => {} } @@ -366,79 +365,80 @@ impl ProjectPanel { fn deploy_context_menu( &mut self, - position: Point, - entry_id: ProjectEntryId, - cx: &mut ViewContext, + _position: Point, + _entry_id: ProjectEntryId, + _cx: &mut ViewContext, ) { - // let project = self.project.read(cx); + todo!() + // let project = self.project.read(cx); - // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { - // id - // } else { - // return; - // }; + // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { + // id + // } else { + // return; + // }; - // self.selection = Some(Selection { - // worktree_id, - // entry_id, - // }); + // self.selection = Some(Selection { + // worktree_id, + // entry_id, + // }); - // let mut menu_entries = Vec::new(); - // if let Some((worktree, entry)) = self.selected_entry(cx) { - // let is_root = Some(entry) == worktree.root_entry(); - // if !project.is_remote() { - // menu_entries.push(ContextMenuItem::action( - // "Add Folder to Project", - // workspace::AddFolderToProject, - // )); - // if is_root { - // let project = self.project.clone(); - // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { - // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); - // })); + // let mut menu_entries = Vec::new(); + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let is_root = Some(entry) == worktree.root_entry(); + // if !project.is_remote() { + // menu_entries.push(ContextMenuItem::action( + // "Add Folder to Project", + // workspace::AddFolderToProject, + // )); + // if is_root { + // let project = self.project.clone(); + // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { + // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); + // })); + // } // } - // } - // menu_entries.push(ContextMenuItem::action("New File", NewFile)); - // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Cut", Cut)); - // menu_entries.push(ContextMenuItem::action("Copy", Copy)); - // if let Some(clipboard_entry) = self.clipboard_entry { - // if clipboard_entry.worktree_id() == worktree.id() { - // menu_entries.push(ContextMenuItem::action("Paste", Paste)); - // } - // } - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); - // menu_entries.push(ContextMenuItem::action( - // "Copy Relative Path", - // CopyRelativePath, - // )); - - // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::action("New File", NewFile)); + // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); // menu_entries.push(ContextMenuItem::Separator); - // } - // menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); - // if entry.is_dir() { - // menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); + // menu_entries.push(ContextMenuItem::action("Cut", Cut)); + // menu_entries.push(ContextMenuItem::action("Copy", Copy)); + // if let Some(clipboard_entry) = self.clipboard_entry { + // if clipboard_entry.worktree_id() == worktree.id() { + // menu_entries.push(ContextMenuItem::action("Paste", Paste)); + // } + // } + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); // menu_entries.push(ContextMenuItem::action( - // "Search Inside", - // NewSearchInDirectory, + // "Copy Relative Path", + // CopyRelativePath, // )); + + // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::Separator); + // } + // menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); + // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); + // menu_entries.push(ContextMenuItem::action( + // "Search Inside", + // NewSearchInDirectory, + // )); + // } + + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Rename", Rename)); + // if !is_root { + // menu_entries.push(ContextMenuItem::action("Delete", Delete)); + // } // } - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Rename", Rename)); - // if !is_root { - // menu_entries.push(ContextMenuItem::action("Delete", Delete)); - // } - // } + // // self.context_menu.update(cx, |menu, cx| { + // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); + // // }); - // // self.context_menu.update(cx, |menu, cx| { - // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); - // // }); - - // cx.notify(); + // cx.notify(); } fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { @@ -955,7 +955,7 @@ impl ProjectPanel { } } - fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { + fn open_in_terminal(&mut self, _: &OpenInTerminal, _cx: &mut ViewContext) { todo!() // if let Some((worktree, entry)) = self.selected_entry(cx) { // let window = cx.window(); @@ -990,36 +990,37 @@ impl ProjectPanel { } } - fn move_entry( - &mut self, - entry_to_move: ProjectEntryId, - destination: ProjectEntryId, - destination_is_file: bool, - cx: &mut ViewContext, - ) { - let destination_worktree = self.project.update(cx, |project, cx| { - let entry_path = project.path_for_entry(entry_to_move, cx)?; - let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + // todo!() + // fn move_entry( + // &mut self, + // entry_to_move: ProjectEntryId, + // destination: ProjectEntryId, + // destination_is_file: bool, + // cx: &mut ViewContext, + // ) { + // let destination_worktree = self.project.update(cx, |project, cx| { + // let entry_path = project.path_for_entry(entry_to_move, cx)?; + // let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); - let mut destination_path = destination_entry_path.as_ref(); - if destination_is_file { - destination_path = destination_path.parent()?; - } + // let mut destination_path = destination_entry_path.as_ref(); + // if destination_is_file { + // destination_path = destination_path.parent()?; + // } - let mut new_path = destination_path.to_path_buf(); - new_path.push(entry_path.path.file_name()?); - if new_path != entry_path.path.as_ref() { - let task = project.rename_entry(entry_to_move, new_path, cx)?; - cx.foreground_executor().spawn(task).detach_and_log_err(cx); - } + // let mut new_path = destination_path.to_path_buf(); + // new_path.push(entry_path.path.file_name()?); + // if new_path != entry_path.path.as_ref() { + // let task = project.rename_entry(entry_to_move, new_path, cx)?; + // cx.foreground_executor().spawn(task).detach_and_log_err(cx); + // } - Some(project.worktree_id_for_entry(destination, cx)?) - }); + // Some(project.worktree_id_for_entry(destination, cx)?) + // }); - if let Some(destination_worktree) = destination_worktree { - self.expand_entry(destination_worktree, destination, cx); - } - } + // if let Some(destination_worktree) = destination_worktree { + // self.expand_entry(destination_worktree, destination, cx); + // } + // } fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { let mut entry_index = 0; @@ -1367,7 +1368,9 @@ impl ProjectPanel { if let (Some(editor), true) = (editor, show_editor) { div().w_full().child(editor.clone()) } else { - div().child(Label::new(details.filename.clone())) + div() + .text_color(filename_text_color) + .child(Label::new(details.filename.clone())) } .ml_1(), ) @@ -1411,21 +1414,16 @@ impl ProjectPanel { } } }) - // .on_down(MouseButton::Right, move |event, this, cx| { - // this.deploy_context_menu(event.position, entry_id, cx); - // }) - // .on_up(MouseButton::Left, move |_, this, cx| { - // if let Some((_, dragged_entry)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { + .on_mouse_down(MouseButton::Right, move |this, event, cx| { + this.deploy_context_menu(event.position, entry_id, cx); + }) + // .on_drop::(|this, event, cx| { // this.move_entry( // *dragged_entry, // entry_id, // matches!(details.kind, EntryKind::File(_)), // cx, // ); - // } // }) } } @@ -1433,10 +1431,7 @@ impl ProjectPanel { impl Render for ProjectPanel { type Element = Div, FocusableKeyDispatch>; - fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { - let theme = cx.theme(); - let last_worktree_root_id = self.last_worktree_root_id; - + fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { let has_worktree = self.visible_entries.len() != 0; if has_worktree { @@ -1475,10 +1470,7 @@ impl Render for ProjectPanel { |this: &mut Self, range, cx| { let mut items = SmallVec::new(); this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(this.render_entry( - id, details, // &mut dragged_entry_destination, - cx, - )); + items.push(this.render_entry(id, details, cx)); }); items }, From 428be43710da5edf5a455e5a4e0551ee5bc8e4b2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 18:50:02 +0100 Subject: [PATCH 094/126] Wire up rename editor --- crates/editor2/src/editor.rs | 332 +++++++++++++++++----------------- crates/editor2/src/element.rs | 13 +- 2 files changed, 177 insertions(+), 168 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ebe78d95b3..3a5b150520 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -40,7 +40,7 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext, - AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, + AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, Entity, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, StatefulInteractive, StatelessInteractive, Styled, Subscription, Task, TextStyle, @@ -100,7 +100,9 @@ use theme::{ use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ - item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, + item::{ItemEvent, ItemHandle}, + searchable::SearchEvent, + ItemNavHistory, SplitDirection, ViewId, Workspace, }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -7690,183 +7692,183 @@ impl Editor { } } - // pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { - // use language::ToOffset as _; + pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + use language::ToOffset as _; - // let project = self.project.clone()?; - // let selection = self.selections.newest_anchor().clone(); - // let (cursor_buffer, cursor_buffer_position) = self - // .buffer - // .read(cx) - // .text_anchor_for_position(selection.head(), cx)?; - // let (tail_buffer, _) = self - // .buffer - // .read(cx) - // .text_anchor_for_position(selection.tail(), cx)?; - // if tail_buffer != cursor_buffer { - // return None; - // } + let project = self.project.clone()?; + let selection = self.selections.newest_anchor().clone(); + let (cursor_buffer, cursor_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.head(), cx)?; + let (tail_buffer, _) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.tail(), cx)?; + if tail_buffer != cursor_buffer { + return None; + } - // let snapshot = cursor_buffer.read(cx).snapshot(); - // let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); - // let prepare_rename = project.update(cx, |project, cx| { - // project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) - // }); + let snapshot = cursor_buffer.read(cx).snapshot(); + let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); + let prepare_rename = project.update(cx, |project, cx| { + project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) + }); - // Some(cx.spawn(|this, mut cx| async move { - // let rename_range = if let Some(range) = prepare_rename.await? { - // Some(range) - // } else { - // this.update(&mut cx, |this, cx| { - // let buffer = this.buffer.read(cx).snapshot(cx); - // let mut buffer_highlights = this - // .document_highlights_for_position(selection.head(), &buffer) - // .filter(|highlight| { - // highlight.start.excerpt_id == selection.head().excerpt_id - // && highlight.end.excerpt_id == selection.head().excerpt_id - // }); - // buffer_highlights - // .next() - // .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor) - // })? - // }; - // if let Some(rename_range) = rename_range { - // let rename_buffer_range = rename_range.to_offset(&snapshot); - // let cursor_offset_in_rename_range = - // cursor_buffer_offset.saturating_sub(rename_buffer_range.start); + Some(cx.spawn(|this, mut cx| async move { + let rename_range = if let Some(range) = prepare_rename.await? { + Some(range) + } else { + this.update(&mut cx, |this, cx| { + let buffer = this.buffer.read(cx).snapshot(cx); + let mut buffer_highlights = this + .document_highlights_for_position(selection.head(), &buffer) + .filter(|highlight| { + highlight.start.excerpt_id == selection.head().excerpt_id + && highlight.end.excerpt_id == selection.head().excerpt_id + }); + buffer_highlights + .next() + .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor) + })? + }; + if let Some(rename_range) = rename_range { + let rename_buffer_range = rename_range.to_offset(&snapshot); + let cursor_offset_in_rename_range = + cursor_buffer_offset.saturating_sub(rename_buffer_range.start); - // this.update(&mut cx, |this, cx| { - // this.take_rename(false, cx); - // let buffer = this.buffer.read(cx).read(cx); - // let cursor_offset = selection.head().to_offset(&buffer); - // let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); - // let rename_end = rename_start + rename_buffer_range.len(); - // let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); - // let mut old_highlight_id = None; - // let old_name: Arc = buffer - // .chunks(rename_start..rename_end, true) - // .map(|chunk| { - // if old_highlight_id.is_none() { - // old_highlight_id = chunk.syntax_highlight_id; - // } - // chunk.text - // }) - // .collect::() - // .into(); + this.update(&mut cx, |this, cx| { + this.take_rename(false, cx); + let buffer = this.buffer.read(cx).read(cx); + let cursor_offset = selection.head().to_offset(&buffer); + let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); + let rename_end = rename_start + rename_buffer_range.len(); + let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); + let mut old_highlight_id = None; + let old_name: Arc = buffer + .chunks(rename_start..rename_end, true) + .map(|chunk| { + if old_highlight_id.is_none() { + old_highlight_id = chunk.syntax_highlight_id; + } + chunk.text + }) + .collect::() + .into(); - // drop(buffer); + drop(buffer); - // // Position the selection in the rename editor so that it matches the current selection. - // this.show_local_selections = false; - // let rename_editor = cx.build_view(|cx| { - // let mut editor = Editor::single_line(cx); - // if let Some(old_highlight_id) = old_highlight_id { - // editor.override_text_style = - // Some(Box::new(move |style| old_highlight_id.style(&style.syntax))); - // } - // editor.buffer.update(cx, |buffer, cx| { - // buffer.edit([(0..0, old_name.clone())], None, cx) - // }); - // editor.select_all(&SelectAll, cx); - // editor - // }); + // Position the selection in the rename editor so that it matches the current selection. + this.show_local_selections = false; + let rename_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, old_name.clone())], None, cx) + }); + editor.select_all(&SelectAll, cx); + editor + }); - // let ranges = this - // .clear_background_highlights::(cx) - // .into_iter() - // .flat_map(|(_, ranges)| ranges.into_iter()) - // .chain( - // this.clear_background_highlights::(cx) - // .into_iter() - // .flat_map(|(_, ranges)| ranges.into_iter()), - // ) - // .collect(); + let ranges = this + .clear_background_highlights::(cx) + .into_iter() + .flat_map(|(_, ranges)| ranges.into_iter()) + .chain( + this.clear_background_highlights::(cx) + .into_iter() + .flat_map(|(_, ranges)| ranges.into_iter()), + ) + .collect(); - // this.highlight_text::( - // ranges, - // HighlightStyle { - // fade_out: Some(style.rename_fade), - // ..Default::default() - // }, - // cx, - // ); - // cx.focus(&rename_editor); - // let block_id = this.insert_blocks( - // [BlockProperties { - // style: BlockStyle::Flex, - // position: range.start.clone(), - // height: 1, - // render: Arc::new({ - // let editor = rename_editor.clone(); - // move |cx: &mut BlockContext| { - // ChildView::new(&editor, cx) - // .contained() - // .with_padding_left(cx.anchor_x) - // .into_any() - // } - // }), - // disposition: BlockDisposition::Below, - // }], - // Some(Autoscroll::fit()), - // cx, - // )[0]; - // this.pending_rename = Some(RenameState { - // range, - // old_name, - // editor: rename_editor, - // block_id, - // }); - // })?; - // } + this.highlight_text::( + ranges, + HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + let rename_focus_handle = rename_editor.focus_handle(cx); + cx.focus(&rename_focus_handle); + let block_id = this.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + position: range.start.clone(), + height: 1, + render: Arc::new({ + let editor = rename_editor.clone(); + move |cx: &mut BlockContext| { + div().pl(cx.anchor_x).child(editor.clone()).render() + } + }), + disposition: BlockDisposition::Below, + }], + Some(Autoscroll::fit()), + cx, + )[0]; + this.pending_rename = Some(RenameState { + range, + old_name, + editor: rename_editor, + block_id, + }); + })?; + } - // Ok(()) - // })) - // } + Ok(()) + })) + } - // pub fn confirm_rename( - // workspace: &mut Workspace, - // _: &ConfirmRename, - // cx: &mut ViewContext, - // ) -> Option>> { - // let editor = workspace.active_item(cx)?.act_as::(cx)?; + pub fn confirm_rename( + &mut self, + _: &ConfirmRename, + cx: &mut ViewContext, + ) -> Option>> { + let rename = self.take_rename(false, cx)?; + let workspace = self.workspace()?; + let (start_buffer, start) = self + .buffer + .read(cx) + .text_anchor_for_position(rename.range.start.clone(), cx)?; + let (end_buffer, end) = self + .buffer + .read(cx) + .text_anchor_for_position(rename.range.end.clone(), cx)?; + if start_buffer != end_buffer { + return None; + } - // let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| { - // let rename = editor.take_rename(false, cx)?; - // let buffer = editor.buffer.read(cx); - // let (start_buffer, start) = - // buffer.text_anchor_for_position(rename.range.start.clone(), cx)?; - // let (end_buffer, end) = - // buffer.text_anchor_for_position(rename.range.end.clone(), cx)?; - // if start_buffer == end_buffer { - // let new_name = rename.editor.read(cx).text(cx); - // Some((start_buffer, start..end, rename.old_name, new_name)) - // } else { - // None - // } - // })?; + let buffer = start_buffer; + let range = start..end; + let old_name = rename.old_name; + let new_name = rename.editor.read(cx).text(cx); - // let rename = workspace.project().clone().update(cx, |project, cx| { - // project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx) - // }); + let rename = workspace + .read(cx) + .project() + .clone() + .update(cx, |project, cx| { + project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx) + }); + let workspace = workspace.downgrade(); - // let editor = editor.downgrade(); - // Some(cx.spawn(|workspace, mut cx| async move { - // let project_transaction = rename.await?; - // Self::open_project_transaction( - // &editor, - // workspace, - // project_transaction, - // format!("Rename: {} → {}", old_name, new_name), - // cx.clone(), - // ) - // .await?; + Some(cx.spawn(|editor, mut cx| async move { + let project_transaction = rename.await?; + Self::open_project_transaction( + &editor, + workspace, + project_transaction, + format!("Rename: {} → {}", old_name, new_name), + cx.clone(), + ) + .await?; - // editor.update(&mut cx, |editor, cx| { - // editor.refresh_document_highlights(cx); - // })?; - // Ok(()) - // })) - // } + editor.update(&mut cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + Ok(()) + })) + } fn take_rename( &mut self, diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 638ed33891..b8e7439ff1 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2007,7 +2007,6 @@ impl EditorElement { anchor_x, gutter_padding, line_height, - // scroll_x, gutter_width, em_width, block_id, @@ -2569,8 +2568,16 @@ impl Element for EditorElement { .confirm_code_action(action, cx) .map(|task| task.detach_and_log_err(cx)); }); - // on_action(cx, Editor::rename); todo!() - // on_action(cx, Editor::confirm_rename); todo!() + register_action(cx, |editor, action, cx| { + editor + .rename(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, |editor, action, cx| { + editor + .confirm_rename(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); register_action(cx, |editor, action, cx| { editor .find_all_references(action, cx) From 251b4640c6e4a03d5cc30d6f2e6a08b7e135b45e Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 14 Nov 2023 12:59:53 -0500 Subject: [PATCH 095/126] Extend tooltip to take meta + kb --- crates/ui2/src/components/tooltip.rs | 35 ++++++++++++++++++++---- crates/workspace2/src/workspace2.rs | 41 ++++++++++++++++++++++------ 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index ee3e9708c0..0d4f10e35e 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,32 +1,57 @@ -use gpui::{div, Div, ParentElement, Render, SharedString, Styled, ViewContext}; +use gpui::{div, Component, Div, ParentElement, Render, SharedString, Styled, ViewContext}; use theme2::ActiveTheme; -use crate::StyledExt; +use crate::{h_stack, v_stack, Label, LabelColor, StyledExt}; + +use super::keybinding; #[derive(Clone, Debug)] pub struct TextTooltip { title: SharedString, + meta: Option, + keybinding: Option, } impl TextTooltip { pub fn new(title: impl Into) -> Self { Self { title: title.into(), + meta: None, + keybinding: None, } } + + pub fn meta(mut self, meta: impl Into) -> Self { + self.meta = Some(meta.into()); + self + } + + pub fn keybinding(mut self, keybinding: impl Into) -> Self { + self.keybinding = Some(keybinding.into()); + self + } } impl Render for TextTooltip { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div() + v_stack() .elevation_2(cx) .font("Zed Sans") - .text_ui() + .text_ui_sm() .text_color(cx.theme().colors().text) .py_1() .px_2() - .child(self.title.clone()) + .child(h_stack().child(self.title.clone()).when_some( + self.keybinding.clone(), + |this, keybinding| { + this.justify_between() + .child(Label::new(keybinding).color(LabelColor::Muted)) + }, + )) + .when_some(self.meta.clone(), |this, meta| { + this.child(Label::new(meta).color(LabelColor::Muted)) + }) } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 575ab6b8bd..eb7ef7608e 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -69,7 +69,7 @@ use std::{ }; use theme2::ActiveTheme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use ui::{h_stack, Button, ButtonVariant, Label, LabelColor}; +use ui::{h_stack, Button, ButtonVariant, Label, LabelColor, TextTooltip}; use util::ResultExt; use uuid::Uuid; use workspace_settings::{AutosaveSetting, WorkspaceSettings}; @@ -2660,17 +2660,42 @@ impl Workspace { h_stack() // TODO - Add player menu .child( - Button::new("player") - .variant(ButtonVariant::Ghost) - .color(Some(LabelColor::Player(0))), + div() + .id("project_owner_indicator") + .child( + Button::new("player") + .variant(ButtonVariant::Ghost) + .color(Some(LabelColor::Player(0))), + ) + .tooltip(move |_, cx| { + cx.build_view(|cx| TextTooltip::new("Toggle following")) + }), ) // TODO - Add project menu - .child(Button::new("project_name").variant(ButtonVariant::Ghost)) + .child( + div() + .id("titlebar_project_menu_button") + .child(Button::new("project_name").variant(ButtonVariant::Ghost)) + .tooltip(move |_, cx| { + cx.build_view(|cx| TextTooltip::new("Recent Projects")) + }), + ) // TODO - Add git menu .child( - Button::new("branch_name") - .variant(ButtonVariant::Ghost) - .color(Some(LabelColor::Muted)), + div() + .id("titlebar_git_menu_button") + .child( + Button::new("branch_name") + .variant(ButtonVariant::Ghost) + .color(Some(LabelColor::Muted)), + ) + .tooltip(move |_, cx| { + cx.build_view(|cx| { + TextTooltip::new("Recent Branches") + .keybinding("⌘B") + .meta("Only local branches shown") + }) + }), ), ) // self.titlebar_item .child(h_stack().child(Label::new("Right side titlebar item"))) From bb584cc7c4e52a2864fc1a0e00891872c011abbe Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 11:00:52 -0700 Subject: [PATCH 096/126] WIP --- .../command_palette2/src/command_palette.rs | 2 + crates/editor2/src/element.rs | 114 +++++++++--------- crates/gpui2/src/elements/div.rs | 11 +- crates/gpui2/src/interactive.rs | 17 ++- crates/gpui2/src/keymap/matcher.rs | 1 + crates/gpui2/src/window.rs | 7 +- 6 files changed, 82 insertions(+), 70 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 435a644669..5d428fba8e 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -32,7 +32,9 @@ pub struct CommandPalette { impl CommandPalette { fn register(workspace: &mut Workspace, _: &mut ViewContext) { + dbg!("registering command palette toggle"); workspace.register_action(|workspace, _: &Toggle, cx| { + dbg!("got cmd-shift-p"); let Some(previous_focus_handle) = cx.focused() else { return; }; diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index dd834b4cd8..3efd399a51 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2446,7 +2446,7 @@ impl Element for EditorElement { type ElementState = (); fn element_id(&self) -> Option { - None + None // todo! can we change the element trait to return an id here from the view context? } fn initialize( @@ -2456,6 +2456,41 @@ impl Element for EditorElement { cx: &mut gpui::ViewContext, ) -> Self::ElementState { editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. + } + + fn layout( + &mut self, + editor: &mut Editor, + element_state: &mut Self::ElementState, + cx: &mut gpui::ViewContext, + ) -> gpui::LayoutId { + let rem_size = cx.rem_size(); + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = match editor.mode { + EditorMode::SingleLine => self.style.text.line_height_in_pixels(cx.rem_size()).into(), + EditorMode::AutoHeight { .. } => todo!(), + EditorMode::Full => relative(1.).into(), + }; + cx.request_layout(&style, None) + } + + fn paint( + &mut self, + bounds: Bounds, + editor: &mut Editor, + element_state: &mut Self::ElementState, + cx: &mut gpui::ViewContext, + ) { + let mut layout = self.compute_layout(editor, cx, bounds); + let gutter_bounds = Bounds { + origin: bounds.origin, + size: layout.gutter_size, + }; + let text_bounds = Bounds { + origin: gutter_bounds.upper_right(), + size: layout.text_size, + }; let dispatch_context = editor.dispatch_context(cx); cx.with_element_id(Some(cx.view().entity_id()), |cx| { @@ -2621,63 +2656,28 @@ impl Element for EditorElement { register_action(cx, Editor::context_menu_prev); register_action(cx, Editor::context_menu_next); register_action(cx, Editor::context_menu_last); + + // We call with_z_index to establish a new stacking context. + cx.with_z_index(0, |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + self.paint_mouse_listeners( + bounds, + gutter_bounds, + text_bounds, + &layout.position_map, + cx, + ); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); + if layout.gutter_size.width > Pixels::ZERO { + self.paint_gutter(gutter_bounds, &mut layout, editor, cx); + } + self.paint_text(text_bounds, &mut layout, editor, cx); + let input_handler = ElementInputHandler::new(bounds, cx); + cx.handle_input(&editor.focus_handle, input_handler); + }); + }); }, - ) - }); - } - - fn layout( - &mut self, - editor: &mut Editor, - element_state: &mut Self::ElementState, - cx: &mut gpui::ViewContext, - ) -> gpui::LayoutId { - let rem_size = cx.rem_size(); - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = match editor.mode { - EditorMode::SingleLine => self.style.text.line_height_in_pixels(cx.rem_size()).into(), - EditorMode::AutoHeight { .. } => todo!(), - EditorMode::Full => relative(1.).into(), - }; - cx.request_layout(&style, None) - } - - fn paint( - &mut self, - bounds: Bounds, - editor: &mut Editor, - element_state: &mut Self::ElementState, - cx: &mut gpui::ViewContext, - ) { - let mut layout = self.compute_layout(editor, cx, bounds); - let gutter_bounds = Bounds { - origin: bounds.origin, - size: layout.gutter_size, - }; - let text_bounds = Bounds { - origin: gutter_bounds.upper_right(), - size: layout.text_size, - }; - - // We call with_z_index to establish a new stacking context. - cx.with_z_index(0, |cx| { - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - self.paint_mouse_listeners( - bounds, - gutter_bounds, - text_bounds, - &layout.position_map, - cx, - ); - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &mut layout, editor, cx); - } - self.paint_text(text_bounds, &mut layout, editor, cx); - let input_handler = ElementInputHandler::new(bounds, cx); - cx.handle_input(&editor.focus_handle, input_handler); - }); + ); }); } } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index aa4ef9bd36..94f94241b2 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -612,6 +612,7 @@ impl Element for Div { let interactive_state = self .interactivity .initialize(element_state.map(|s| s.interactive_state), cx); + for child in &mut self.children { child.initialize(view_state, cx); } @@ -763,6 +764,7 @@ where .unwrap_or_else(|| cx.focus_handle()) }); } + element_state } @@ -773,11 +775,7 @@ where f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, ) -> LayoutId { let style = self.compute_style(None, element_state, cx); - cx.with_key_dispatch( - self.key_context.clone(), - self.tracked_focus_handle.clone(), - |_, cx| f(style, cx), - ) + f(style, cx) } pub fn paint( @@ -1078,6 +1076,9 @@ where }) } + if !self.key_context.is_empty() { + dbg!(&self.key_context, self.action_listeners.len()); + } for (action_type, listener) in self.action_listeners.drain(..) { cx.on_action(action_type, listener) } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 1896957ac8..013ed2ea48 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -286,8 +286,8 @@ pub struct FocusEvent { #[cfg(test)] mod test { use crate::{ - self as gpui, div, Div, FocusHandle, InteractiveComponent, KeyBinding, Keystroke, - ParentComponent, Render, Stateful, TestAppContext, VisualContext, + self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding, + Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext, }; struct TestView { @@ -304,10 +304,15 @@ mod test { fn render(&mut self, _: &mut gpui::ViewContext) -> Self::Element { div().id("testview").child( div() - .key_context("test") - .track_focus(&self.focus_handle) + .key_context("parent") .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true) - .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true), + .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true) + .child(|this: &mut Self, _cx: &mut ViewContext| { + div() + .key_context("nested") + .track_focus(&this.focus_handle) + .render() + }), ) } } @@ -325,7 +330,7 @@ mod test { }); cx.update(|cx| { - cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, None)]); + cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]); }); window diff --git a/crates/gpui2/src/keymap/matcher.rs b/crates/gpui2/src/keymap/matcher.rs index bab9c0f575..a842913aef 100644 --- a/crates/gpui2/src/keymap/matcher.rs +++ b/crates/gpui2/src/keymap/matcher.rs @@ -97,6 +97,7 @@ impl KeystrokeMatcher { } } +#[derive(Debug)] pub enum KeyMatch { None, Pending, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 4ed7f89c78..3832605c3d 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1309,6 +1309,7 @@ impl<'a> WindowContext<'a> { .current_frame .dispatch_tree .dispatch_path(node_id); + dbg!(node_id, &dispatch_path, self.propagate_event); // Capture phase for node_id in &dispatch_path { @@ -1328,6 +1329,8 @@ impl<'a> WindowContext<'a> { } } + dbg!(node_id, &dispatch_path, self.propagate_event); + // Bubble phase for node_id in dispatch_path.iter().rev() { let node = self.window.current_frame.dispatch_tree.node(*node_id); @@ -1337,6 +1340,7 @@ impl<'a> WindowContext<'a> { } in node.action_listeners.clone() { let any_action = action.as_any(); + dbg!(action_type, any_action.type_id()); if action_type == any_action.type_id() { self.propagate_event = false; // Actions stop propagation by default during the bubble phase listener(any_action, DispatchPhase::Bubble, self); @@ -2082,11 +2086,10 @@ impl<'a, V: 'static> ViewContext<'a, V> { f: impl FnOnce(Option, &mut Self) -> R, ) -> R { let window = &mut self.window; - window .current_frame .dispatch_tree - .push_node(context, &mut window.previous_frame.dispatch_tree); + .push_node(context.clone(), &mut window.previous_frame.dispatch_tree); if let Some(focus_handle) = focus_handle.as_ref() { window .current_frame From f4ccff7b726eb38b9f41ce652a8bea841ac76014 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 11:03:55 -0700 Subject: [PATCH 097/126] TEMP --- Cargo.lock | 1 + crates/file_finder2/Cargo.toml | 1 + crates/file_finder2/src/file_finder.rs | 91 ++++++++++++++++++++++---- crates/picker2/src/picker2.rs | 11 +++- 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbc88e6785..2e260f1e49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3081,6 +3081,7 @@ dependencies = [ "settings2", "text2", "theme2", + "ui2", "util", "workspace2", ] diff --git a/crates/file_finder2/Cargo.toml b/crates/file_finder2/Cargo.toml index 8950cff792..22b9f2cbc8 100644 --- a/crates/file_finder2/Cargo.toml +++ b/crates/file_finder2/Cargo.toml @@ -20,6 +20,7 @@ settings = { package = "settings2", path = "../settings2" } text = { package = "text2", path = "../text2" } util = { path = "../util" } theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } workspace = { package = "workspace2", path = "../workspace2" } postage.workspace = true serde.workspace = true diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index a9b5be1dcd..67fb1e400f 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -1,7 +1,9 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; -use gpui::{actions, AppContext, Task, ViewContext, View, EventEmitter, WindowContext}; +use gpui::{ + actions, AppContext, Div, EventEmitter, Render, Task, View, ViewContext, WindowContext, +}; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use std::{ @@ -12,13 +14,13 @@ use std::{ }, }; use text::Point; -use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; -use workspace::{Workspace, Modal, ModalEvent}; +use util::{paths::PathLikeWithPosition, post_inc}; +use workspace::{Modal, ModalEvent, Workspace}; actions!(Toggle); pub struct FileFinder { - picker: View> + picker: View>, } pub fn init(cx: &mut AppContext) { @@ -28,21 +30,88 @@ pub fn init(cx: &mut AppContext) { impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|workspace, _: &Toggle, cx| { - workspace.toggle_modal(cx, |cx| FileFinder::new(cx)); + let Some(file_finder) = workspace.current_modal::(cx) else { + workspace.toggle_modal(cx, |cx| FileFinder::new(workspace, cx)); + return; + }; + file_finder.update(cx, |file_finder, cx| { + file_finder + .picker + .update(cx, |picker, cx| picker.cycle_selection(cx)) + }) }); } - fn new(cx: &mut ViewContext) -> Self { - FileFinder{ + fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> Self { + let project = workspace.project().read(cx); - } + let currently_opened_path = workspace + .active_item(cx) + .and_then(|item| item.project_path(cx)) + .map(|project_path| { + let abs_path = project + .worktree_for_id(project_path.worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)); + FoundPath::new(project_path, abs_path) + }); + + // if exists, bubble the currently opened path to the top + let history_items = currently_opened_path + .clone() + .into_iter() + .chain( + workspace + .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx) + .into_iter() + .filter(|(history_path, _)| { + Some(history_path) + != currently_opened_path + .as_ref() + .map(|found_path| &found_path.project) + }) + .filter(|(_, history_abs_path)| { + history_abs_path.as_ref() + != currently_opened_path + .as_ref() + .and_then(|found_path| found_path.absolute.as_ref()) + }) + .filter(|(_, history_abs_path)| match history_abs_path { + Some(abs_path) => history_file_exists(abs_path), + None => true, + }) + .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)), + ) + .collect(); + + let project = workspace.project().clone(); + let workspace = cx.handle().downgrade(); + let finder = cx.add_view(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace, + project, + currently_opened_path, + history_items, + cx, + ), + cx, + ) + }); + finder } } -impl EventEmitter for FileFinder; -impl Modal for FileFinder{ +impl EventEmitter for FileFinder {} +impl Modal for FileFinder { fn focus(&self, cx: &mut WindowContext) { - self.picker.update(cx, |picker, cx| { picker.focus(cx) }) + self.picker.update(cx, |picker, cx| picker.focus(cx)) + } +} +impl Render for FileFinder { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + v_stack().w_96().child(self.picker.clone()) } } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 0cfe5c8992..97f4262623 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -58,7 +58,7 @@ impl Picker { self.editor.update(cx, |editor, cx| editor.focus(cx)); } - fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { + pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { let count = self.delegate.match_count(); if count > 0 { let index = self.delegate.selected_index(); @@ -98,6 +98,15 @@ impl Picker { } } + pub fn cycle_selection(&mut self, cx: &mut ViewContext) { + let count = self.delegate.match_count(); + let index = self.delegate.selected_index(); + let new_index = if index + 1 == count { 0 } else { index + 1 }; + self.delegate.set_selected_index(new_index, cx); + self.scroll_handle.scroll_to_item(new_index); + cx.notify(); + } + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { self.delegate.dismissed(cx); } From b69b5742eda00fcef9693d0131ac3282bf36837a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 11:27:52 -0700 Subject: [PATCH 098/126] Fix panicking unwrap() --- crates/command_palette2/src/command_palette.rs | 2 -- crates/gpui2/src/elements/div.rs | 3 --- crates/gpui2/src/elements/uniform_list.rs | 6 +++++- crates/gpui2/src/window.rs | 4 ---- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 5d428fba8e..435a644669 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -32,9 +32,7 @@ pub struct CommandPalette { impl CommandPalette { fn register(workspace: &mut Workspace, _: &mut ViewContext) { - dbg!("registering command palette toggle"); workspace.register_action(|workspace, _: &Toggle, cx| { - dbg!("got cmd-shift-p"); let Some(previous_focus_handle) = cx.focused() else { return; }; diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 94f94241b2..f3f6385503 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1076,9 +1076,6 @@ where }) } - if !self.key_context.is_empty() { - dbg!(&self.key_context, self.action_listeners.len()); - } for (action_type, listener) in self.action_listeners.drain(..) { cx.on_action(action_type, listener) } diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 2f6584cda5..28292a3d00 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -193,7 +193,11 @@ impl Element for UniformList { }; let mut interactivity = mem::take(&mut self.interactivity); - let shared_scroll_offset = element_state.interactive.scroll_offset.clone().unwrap(); + let shared_scroll_offset = element_state + .interactive + .scroll_offset + .get_or_insert_with(Arc::default) + .clone(); interactivity.paint( bounds, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 3832605c3d..00e050f2d8 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1309,7 +1309,6 @@ impl<'a> WindowContext<'a> { .current_frame .dispatch_tree .dispatch_path(node_id); - dbg!(node_id, &dispatch_path, self.propagate_event); // Capture phase for node_id in &dispatch_path { @@ -1329,8 +1328,6 @@ impl<'a> WindowContext<'a> { } } - dbg!(node_id, &dispatch_path, self.propagate_event); - // Bubble phase for node_id in dispatch_path.iter().rev() { let node = self.window.current_frame.dispatch_tree.node(*node_id); @@ -1340,7 +1337,6 @@ impl<'a> WindowContext<'a> { } in node.action_listeners.clone() { let any_action = action.as_any(); - dbg!(action_type, any_action.type_id()); if action_type == any_action.type_id() { self.propagate_event = false; // Actions stop propagation by default during the bubble phase listener(any_action, DispatchPhase::Bubble, self); From 27574524b857881227962daae95be05bcfb8b056 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 10:31:55 -0800 Subject: [PATCH 099/126] Restore quit action --- crates/gpui2/src/window.rs | 6 ++++ crates/zed2/src/zed2.rs | 63 ++++++++++++++++++++++---------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 4ad807b357..efb586fe03 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2368,6 +2368,12 @@ impl WindowHandle { { cx.read_window(self, |root_view, _cx| root_view.clone()) } + + pub fn is_active(&self, cx: &WindowContext) -> Option { + cx.windows + .get(self.id) + .and_then(|window| window.as_ref().map(|window| window.active)) + } } impl Copy for WindowHandle {} diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 8f4a9c6ddf..73faeaaaf4 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -460,36 +460,45 @@ fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext) { .collect::>() })?; - // // If multiple windows have unsaved changes, and need a save prompt, - // // prompt in the active window before switching to a different window. - // workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + // If multiple windows have unsaved changes, and need a save prompt, + // prompt in the active window before switching to a different window. + cx.update(|_, cx| { + workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + }) + .log_err(); - // if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { - // let answer = window.prompt( - // PromptLevel::Info, - // "Are you sure you want to quit?", - // &["Quit", "Cancel"], - // &mut cx, - // ); + if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { + let answer = cx + .update(|_, cx| { + cx.prompt( + PromptLevel::Info, + "Are you sure you want to quit?", + &["Quit", "Cancel"], + ) + }) + .log_err(); - // if let Some(mut answer) = answer { - // let answer = answer.next().await; - // if answer != Some(0) { - // return Ok(()); - // } - // } - // } + if let Some(mut answer) = answer { + let answer = answer.await.ok(); + if answer != Some(0) { + return Ok(()); + } + } + } - // // If the user cancels any save prompt, then keep the app open. - // for window in workspace_windows { - // if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { - // workspace.prepare_to_close(true, cx) - // }) { - // if !should_close.await? { - // return Ok(()); - // } - // } - // } + // If the user cancels any save prompt, then keep the app open. + for window in workspace_windows { + if let Some(should_close) = window + .update(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) + }) + .log_err() + { + if !should_close.await? { + return Ok(()); + } + } + } cx.update(|_, cx| { cx.quit(); })?; From 90d7033fd046cfb29659da420ebc022679f2e408 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 14 Nov 2023 13:36:03 -0500 Subject: [PATCH 100/126] Pass `KeyBinding`s to `TextTooltip`s --- crates/ui2/src/components/keybinding.rs | 2 +- crates/ui2/src/components/tooltip.rs | 30 ++++++++++++------------- crates/workspace2/src/workspace2.rs | 12 ++++++++-- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index a3e5a870a6..04e036f365 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -3,7 +3,7 @@ use strum::EnumIter; use crate::prelude::*; -#[derive(Component)] +#[derive(Component, Clone)] pub struct KeyBinding { /// A keybinding consists of a key and a set of modifier keys. /// More then one keybinding produces a chord. diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 0d4f10e35e..8f31d77b67 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,15 +1,13 @@ -use gpui::{div, Component, Div, ParentElement, Render, SharedString, Styled, ViewContext}; +use gpui::{Div, Render}; use theme2::ActiveTheme; -use crate::{h_stack, v_stack, Label, LabelColor, StyledExt}; +use crate::prelude::*; +use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor, StyledExt}; -use super::keybinding; - -#[derive(Clone, Debug)] pub struct TextTooltip { title: SharedString, meta: Option, - keybinding: Option, + key_binding: Option, } impl TextTooltip { @@ -17,7 +15,7 @@ impl TextTooltip { Self { title: title.into(), meta: None, - keybinding: None, + key_binding: None, } } @@ -26,8 +24,8 @@ impl TextTooltip { self } - pub fn keybinding(mut self, keybinding: impl Into) -> Self { - self.keybinding = Some(keybinding.into()); + pub fn key_binding(mut self, key_binding: impl Into>) -> Self { + self.key_binding = key_binding.into(); self } } @@ -43,13 +41,13 @@ impl Render for TextTooltip { .text_color(cx.theme().colors().text) .py_1() .px_2() - .child(h_stack().child(self.title.clone()).when_some( - self.keybinding.clone(), - |this, keybinding| { - this.justify_between() - .child(Label::new(keybinding).color(LabelColor::Muted)) - }, - )) + .child( + h_stack() + .child(self.title.clone()) + .when_some(self.key_binding.clone(), |this, key_binding| { + this.justify_between().child(key_binding) + }), + ) .when_some(self.meta.clone(), |this, meta| { this.child(Label::new(meta).color(LabelColor::Muted)) }) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index a036b030c9..88e8dc7934 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -69,7 +69,7 @@ use std::{ }; use theme2::ActiveTheme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use ui::{h_stack, Button, ButtonVariant, Label, LabelColor, TextTooltip}; +use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, LabelColor, TextTooltip}; use util::ResultExt; use uuid::Uuid; use workspace_settings::{AutosaveSetting, WorkspaceSettings}; @@ -2502,9 +2502,17 @@ impl Workspace { .color(Some(LabelColor::Muted)), ) .tooltip(move |_, cx| { + // todo!() Replace with real action. + #[gpui::action] + struct NoAction {} + cx.build_view(|cx| { TextTooltip::new("Recent Branches") - .keybinding("⌘B") + .key_binding(KeyBinding::new(gpui::KeyBinding::new( + "cmd-b", + NoAction {}, + None, + ))) .meta("Only local branches shown") }) }), From 9d31523cf3056df980d47ff44f3439a0ce15d51e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 14 Nov 2023 13:37:21 -0500 Subject: [PATCH 101/126] Rename `keybinding` method on `PaletteItem` to `key_binding` --- crates/ui2/src/components/palette.rs | 25 +++++++++++-------------- crates/ui2/src/static_data.rs | 20 ++++++++++---------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 4e1034595d..d73b15940e 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -108,7 +108,7 @@ impl Palette { pub struct PaletteItem { pub label: SharedString, pub sublabel: Option, - pub keybinding: Option, + pub key_binding: Option, } impl PaletteItem { @@ -116,7 +116,7 @@ impl PaletteItem { Self { label: label.into(), sublabel: None, - keybinding: None, + key_binding: None, } } @@ -130,11 +130,8 @@ impl PaletteItem { self } - pub fn keybinding(mut self, keybinding: K) -> Self - where - K: Into>, - { - self.keybinding = keybinding.into(); + pub fn key_binding(mut self, key_binding: impl Into>) -> Self { + self.key_binding = key_binding.into(); self } @@ -149,7 +146,7 @@ impl PaletteItem { .child(Label::new(self.label.clone())) .children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))), ) - .children(self.keybinding) + .children(self.key_binding) } } @@ -182,23 +179,23 @@ mod stories { .placeholder("Execute a command...") .items(vec![ PaletteItem::new("theme selector: toggle") - .keybinding(KeyBinding::new(binding("cmd-k cmd-t"))), + .key_binding(KeyBinding::new(binding("cmd-k cmd-t"))), PaletteItem::new("assistant: inline assist") - .keybinding(KeyBinding::new(binding("cmd-enter"))), + .key_binding(KeyBinding::new(binding("cmd-enter"))), PaletteItem::new("assistant: quote selection") - .keybinding(KeyBinding::new(binding("cmd-<"))), + .key_binding(KeyBinding::new(binding("cmd-<"))), PaletteItem::new("assistant: toggle focus") - .keybinding(KeyBinding::new(binding("cmd-?"))), + .key_binding(KeyBinding::new(binding("cmd-?"))), PaletteItem::new("auto update: check"), PaletteItem::new("auto update: view release notes"), PaletteItem::new("branches: open recent") - .keybinding(KeyBinding::new(binding("cmd-alt-b"))), + .key_binding(KeyBinding::new(binding("cmd-alt-b"))), PaletteItem::new("chat panel: toggle focus"), PaletteItem::new("cli: install"), PaletteItem::new("client: sign in"), PaletteItem::new("client: sign out"), PaletteItem::new("editor: cancel") - .keybinding(KeyBinding::new(binding("escape"))), + .key_binding(KeyBinding::new(binding("escape"))), ]), ) } diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index 89aef8140a..4615adbfa4 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -701,16 +701,16 @@ pub fn static_collab_panel_channels() -> Vec { pub fn example_editor_actions() -> Vec { vec![ - PaletteItem::new("New File").keybinding(KeyBinding::new(binding("cmd-n"))), - PaletteItem::new("Open File").keybinding(KeyBinding::new(binding("cmd-o"))), - PaletteItem::new("Save File").keybinding(KeyBinding::new(binding("cmd-s"))), - PaletteItem::new("Cut").keybinding(KeyBinding::new(binding("cmd-x"))), - PaletteItem::new("Copy").keybinding(KeyBinding::new(binding("cmd-c"))), - PaletteItem::new("Paste").keybinding(KeyBinding::new(binding("cmd-v"))), - PaletteItem::new("Undo").keybinding(KeyBinding::new(binding("cmd-z"))), - PaletteItem::new("Redo").keybinding(KeyBinding::new(binding("cmd-shift-z"))), - PaletteItem::new("Find").keybinding(KeyBinding::new(binding("cmd-f"))), - PaletteItem::new("Replace").keybinding(KeyBinding::new(binding("cmd-r"))), + PaletteItem::new("New File").key_binding(KeyBinding::new(binding("cmd-n"))), + PaletteItem::new("Open File").key_binding(KeyBinding::new(binding("cmd-o"))), + PaletteItem::new("Save File").key_binding(KeyBinding::new(binding("cmd-s"))), + PaletteItem::new("Cut").key_binding(KeyBinding::new(binding("cmd-x"))), + PaletteItem::new("Copy").key_binding(KeyBinding::new(binding("cmd-c"))), + PaletteItem::new("Paste").key_binding(KeyBinding::new(binding("cmd-v"))), + PaletteItem::new("Undo").key_binding(KeyBinding::new(binding("cmd-z"))), + PaletteItem::new("Redo").key_binding(KeyBinding::new(binding("cmd-shift-z"))), + PaletteItem::new("Find").key_binding(KeyBinding::new(binding("cmd-f"))), + PaletteItem::new("Replace").key_binding(KeyBinding::new(binding("cmd-r"))), PaletteItem::new("Jump to Line"), PaletteItem::new("Select All"), PaletteItem::new("Deselect All"), From 48b3a90fbf0ca097b29939a9099f92f7094bef82 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 19:42:07 +0100 Subject: [PATCH 102/126] WIP --- crates/editor2/src/display_map/block_map.rs | 3 ++- crates/editor2/src/editor.rs | 30 +++++++++++++++++++-- crates/editor2/src/element.rs | 7 +++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs index 2f65903f08..05106dd2a1 100644 --- a/crates/editor2/src/display_map/block_map.rs +++ b/crates/editor2/src/display_map/block_map.rs @@ -2,7 +2,7 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, Highlights, }; -use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; +use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; use gpui::{AnyElement, Pixels, ViewContext}; use language::{BufferSnapshot, Chunk, Patch, Point}; @@ -88,6 +88,7 @@ pub struct BlockContext<'a, 'b> { pub em_width: Pixels, pub line_height: Pixels, pub block_id: usize, + pub editor_style: &'b EditorStyle, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 3a5b150520..23dfc9b9d3 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -7796,9 +7796,35 @@ impl Editor { position: range.start.clone(), height: 1, render: Arc::new({ - let editor = rename_editor.clone(); + let rename_editor = rename_editor.clone(); move |cx: &mut BlockContext| { - div().pl(cx.anchor_x).child(editor.clone()).render() + let text_style = if let Some(highlight_style) = old_highlight_id + .and_then(|h| h.style(&cx.editor_style.syntax)) + { + cx.editor_style + .text + .clone() + .highlight(highlight_style) + .unwrap_or_else(|_| cx.editor_style.text.clone()) + } else { + cx.editor_style.text.clone() + }; + div().pl(cx.anchor_x).child(with_view( + &rename_editor, + |_, _| { + EditorElement::new(EditorStyle { + background: cx.theme().system().transparent, + local_player: cx.editor_style.local_player, + text: text_style, + scrollbar_width: cx.editor_style.scrollbar_width, + syntax: cx.editor_style.syntax.clone(), + diagnostic_style: cx + .editor_style + .diagnostic_style + .clone(), + }) + }, + )) } }), disposition: BlockDisposition::Below, diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index b8e7439ff1..5cdedd1de7 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2010,6 +2010,7 @@ impl EditorElement { gutter_width, em_width, block_id, + editor_style: &self.style, }) } TransformBlock::ExcerptHeader { @@ -2658,6 +2659,12 @@ impl Element for EditorElement { } } +impl Component for EditorElement { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + // impl EditorElement { // type LayoutState = LayoutState; // type PaintState = (); From dc56a7b12bf511eafb0f472d9c6f359e9fc41a19 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 14 Nov 2023 13:43:37 -0500 Subject: [PATCH 103/126] Add `LabelSize` --- crates/ui2/src/components/label.rs | 30 +++++++++++++++++++++++++++- crates/ui2/src/components/tooltip.rs | 8 ++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 4b9cea8dc2..c316a07483 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -3,6 +3,13 @@ use gpui::{relative, Hsla, Text, TextRun, WindowContext}; use crate::prelude::*; use crate::styled_ext::StyledExt; +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] +pub enum LabelSize { + #[default] + Default, + Small, +} + #[derive(Default, PartialEq, Copy, Clone)] pub enum LabelColor { #[default] @@ -56,6 +63,7 @@ pub enum LineHeightStyle { #[derive(Component)] pub struct Label { label: SharedString, + size: LabelSize, line_height_style: LineHeightStyle, color: LabelColor, strikethrough: bool, @@ -65,12 +73,18 @@ impl Label { pub fn new(label: impl Into) -> Self { Self { label: label.into(), + size: LabelSize::Default, line_height_style: LineHeightStyle::default(), color: LabelColor::Default, strikethrough: false, } } + pub fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + pub fn color(mut self, color: LabelColor) -> Self { self.color = color; self @@ -98,7 +112,10 @@ impl Label { .bg(LabelColor::Hidden.hsla(cx)), ) }) - .text_ui() + .map(|this| match self.size { + LabelSize::Default => this.text_ui(), + LabelSize::Small => this.text_ui_sm(), + }) .when(self.line_height_style == LineHeightStyle::UILabel, |this| { this.line_height(relative(1.)) }) @@ -110,6 +127,7 @@ impl Label { #[derive(Component)] pub struct HighlightedLabel { label: SharedString, + size: LabelSize, color: LabelColor, highlight_indices: Vec, strikethrough: bool, @@ -121,12 +139,18 @@ impl HighlightedLabel { pub fn new(label: impl Into, highlight_indices: Vec) -> Self { Self { label: label.into(), + size: LabelSize::Default, color: LabelColor::Default, highlight_indices, strikethrough: false, } } + pub fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + pub fn color(mut self, color: LabelColor) -> Self { self.color = color; self @@ -186,6 +210,10 @@ impl HighlightedLabel { .bg(LabelColor::Hidden.hsla(cx)), ) }) + .map(|this| match self.size { + LabelSize::Default => this.text_ui(), + LabelSize::Small => this.text_ui_sm(), + }) .child(Text::styled(self.label, runs)) } } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 8f31d77b67..536bb22ba0 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,8 +1,8 @@ use gpui::{Div, Render}; use theme2::ActiveTheme; -use crate::prelude::*; use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor, StyledExt}; +use crate::{prelude::*, LabelSize}; pub struct TextTooltip { title: SharedString, @@ -49,7 +49,11 @@ impl Render for TextTooltip { }), ) .when_some(self.meta.clone(), |this, meta| { - this.child(Label::new(meta).color(LabelColor::Muted)) + this.child( + Label::new(meta) + .size(LabelSize::Small) + .color(LabelColor::Muted), + ) }) } } From 76c15229c18f9a008209a280212d55ff5fe85930 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 14 Nov 2023 13:48:01 -0500 Subject: [PATCH 104/126] Combine `LabelColor` and `IconColor` into `TextColor` --- crates/editor2/src/items.rs | 4 +- crates/go_to_line2/src/go_to_line.rs | 4 +- crates/picker2/src/picker2.rs | 4 +- crates/ui2/src/components/button.rs | 44 ++++++----- crates/ui2/src/components/checkbox.rs | 10 +-- crates/ui2/src/components/icon.rs | 74 ++----------------- crates/ui2/src/components/icon_button.rs | 10 +-- crates/ui2/src/components/input.rs | 10 +-- crates/ui2/src/components/label.rs | 56 +++++++------- crates/ui2/src/components/list.rs | 20 ++--- crates/ui2/src/components/palette.rs | 6 +- crates/ui2/src/components/tab.rs | 16 ++-- crates/ui2/src/components/toggle.rs | 6 +- crates/ui2/src/components/tooltip.rs | 6 +- crates/ui2/src/prelude.rs | 2 +- crates/ui2/src/static_data.rs | 36 ++++----- crates/ui2/src/to_extract/buffer_search.rs | 4 +- crates/ui2/src/to_extract/chat_panel.rs | 4 +- crates/ui2/src/to_extract/copilot.rs | 4 +- crates/ui2/src/to_extract/editor_pane.rs | 4 +- .../ui2/src/to_extract/notifications_panel.rs | 14 ++-- crates/ui2/src/to_extract/status_bar.rs | 12 +-- crates/ui2/src/to_extract/title_bar.rs | 10 +-- crates/workspace2/src/pane.rs | 6 +- crates/workspace2/src/workspace2.rs | 6 +- 25 files changed, 152 insertions(+), 220 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 9614082ccf..3a9e6c2a65 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -30,7 +30,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; -use ui::{Label, LabelColor}; +use ui::{Label, TextColor}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::{ @@ -607,7 +607,7 @@ impl Item for Editor { &description, MAX_TAB_TITLE_LEN, )) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), ) })), diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 1d57be6fd0..a16ff85ff2 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -5,7 +5,7 @@ use gpui::{ }; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Label, LabelColor, StyledExt}; +use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::{Modal, ModalEvent, Workspace}; @@ -176,7 +176,7 @@ impl Render for GoToLine { .justify_between() .px_2() .py_1() - .child(Label::new(self.current_text.clone()).color(LabelColor::Muted)), + .child(Label::new(self.current_text.clone()).color(TextColor::Muted)), ), ) } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 0cfe5c8992..70cef67868 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -4,7 +4,7 @@ use gpui::{ Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; use std::{cmp, sync::Arc}; -use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; +use ui::{prelude::*, v_stack, Divider, Label, TextColor}; pub struct Picker { pub delegate: D, @@ -224,7 +224,7 @@ impl Render for Picker { v_stack().p_1().grow().child( div() .px_1() - .child(Label::new("No matches").color(LabelColor::Muted)), + .child(Label::new("No matches").color(TextColor::Muted)), ), ) }) diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 1418a977f1..f3f3ba6a50 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -2,10 +2,8 @@ use std::sync::Arc; use gpui::{div, DefiniteLength, Hsla, MouseButton, WindowContext}; -use crate::{ - h_stack, prelude::*, Icon, IconButton, IconColor, IconElement, Label, LabelColor, - LineHeightStyle, -}; +use crate::prelude::*; +use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor}; /// Provides the flexibility to use either a standard /// button or an icon button in a given context. @@ -87,7 +85,7 @@ pub struct Button { label: SharedString, variant: ButtonVariant, width: Option, - color: Option, + color: Option, } impl Button { @@ -141,14 +139,14 @@ impl Button { self } - pub fn color(mut self, color: Option) -> Self { + pub fn color(mut self, color: Option) -> Self { self.color = color; self } - pub fn label_color(&self, color: Option) -> LabelColor { + pub fn label_color(&self, color: Option) -> TextColor { if self.disabled { - LabelColor::Disabled + TextColor::Disabled } else if let Some(color) = color { color } else { @@ -156,21 +154,21 @@ impl Button { } } - fn render_label(&self, color: LabelColor) -> Label { + fn render_label(&self, color: TextColor) -> Label { Label::new(self.label.clone()) .color(color) .line_height_style(LineHeightStyle::UILabel) } - fn render_icon(&self, icon_color: IconColor) -> Option { + fn render_icon(&self, icon_color: TextColor) -> Option { self.icon.map(|i| IconElement::new(i).color(icon_color)) } pub fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let (icon_color, label_color) = match (self.disabled, self.color) { - (true, _) => (IconColor::Disabled, LabelColor::Disabled), - (_, None) => (IconColor::Default, LabelColor::Default), - (_, Some(color)) => (IconColor::from(color), color), + (true, _) => (TextColor::Disabled, TextColor::Disabled), + (_, None) => (TextColor::Default, TextColor::Default), + (_, Some(color)) => (TextColor::from(color), color), }; let mut button = h_stack() @@ -240,7 +238,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { use super::*; - use crate::{h_stack, v_stack, LabelColor, Story}; + use crate::{h_stack, v_stack, Story, TextColor}; use gpui::{rems, Div, Render}; use strum::IntoEnumIterator; @@ -265,7 +263,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label").variant(ButtonVariant::Ghost), // .state(state), @@ -276,7 +274,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -290,7 +288,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -307,7 +305,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label").variant(ButtonVariant::Filled), // .state(state), @@ -318,7 +316,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -332,7 +330,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -349,7 +347,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -363,7 +361,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -379,7 +377,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs index 20dad74712..3480c8cb72 100644 --- a/crates/ui2/src/components/checkbox.rs +++ b/crates/ui2/src/components/checkbox.rs @@ -6,7 +6,7 @@ use gpui::{ }; use theme2::ActiveTheme; -use crate::{Icon, IconColor, IconElement, Selection}; +use crate::{Icon, IconElement, Selection, TextColor}; pub type CheckHandler = Arc) + Send + Sync>; @@ -58,9 +58,9 @@ impl Checkbox { .color( // If the checkbox is disabled we change the color of the icon. if self.disabled { - IconColor::Disabled + TextColor::Disabled } else { - IconColor::Selected + TextColor::Selected }, ), ) @@ -73,9 +73,9 @@ impl Checkbox { .color( // If the checkbox is disabled we change the color of the icon. if self.disabled { - IconColor::Disabled + TextColor::Disabled } else { - IconColor::Selected + TextColor::Selected }, ), ) diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 75c8129608..5b60421205 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -1,7 +1,7 @@ -use gpui::{rems, svg, Hsla}; +use gpui::{rems, svg}; use strum::EnumIter; -use crate::{prelude::*, LabelColor}; +use crate::prelude::*; #[derive(Default, PartialEq, Copy, Clone)] pub enum IconSize { @@ -10,70 +10,6 @@ pub enum IconSize { Medium, } -#[derive(Default, PartialEq, Copy, Clone)] -pub enum IconColor { - #[default] - Default, - Accent, - Created, - Deleted, - Disabled, - Error, - Hidden, - Info, - Modified, - Muted, - Placeholder, - Player(u32), - Selected, - Success, - Warning, -} - -impl IconColor { - pub fn color(self, cx: &WindowContext) -> Hsla { - match self { - IconColor::Default => cx.theme().colors().icon, - IconColor::Muted => cx.theme().colors().icon_muted, - IconColor::Disabled => cx.theme().colors().icon_disabled, - IconColor::Placeholder => cx.theme().colors().icon_placeholder, - IconColor::Accent => cx.theme().colors().icon_accent, - IconColor::Error => cx.theme().status().error, - IconColor::Warning => cx.theme().status().warning, - IconColor::Success => cx.theme().status().success, - IconColor::Info => cx.theme().status().info, - IconColor::Selected => cx.theme().colors().icon_accent, - IconColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, - IconColor::Created => cx.theme().status().created, - IconColor::Modified => cx.theme().status().modified, - IconColor::Deleted => cx.theme().status().deleted, - IconColor::Hidden => cx.theme().status().hidden, - } - } -} - -impl From for IconColor { - fn from(label: LabelColor) -> Self { - match label { - LabelColor::Default => IconColor::Default, - LabelColor::Muted => IconColor::Muted, - LabelColor::Disabled => IconColor::Disabled, - LabelColor::Placeholder => IconColor::Placeholder, - LabelColor::Accent => IconColor::Accent, - LabelColor::Error => IconColor::Error, - LabelColor::Warning => IconColor::Warning, - LabelColor::Success => IconColor::Success, - LabelColor::Info => IconColor::Info, - LabelColor::Selected => IconColor::Selected, - LabelColor::Player(i) => IconColor::Player(i), - LabelColor::Created => IconColor::Created, - LabelColor::Modified => IconColor::Modified, - LabelColor::Deleted => IconColor::Deleted, - LabelColor::Hidden => IconColor::Hidden, - } - } -} - #[derive(Debug, PartialEq, Copy, Clone, EnumIter)] pub enum Icon { Ai, @@ -194,7 +130,7 @@ impl Icon { #[derive(Component)] pub struct IconElement { icon: Icon, - color: IconColor, + color: TextColor, size: IconSize, } @@ -202,12 +138,12 @@ impl IconElement { pub fn new(icon: Icon) -> Self { Self { icon, - color: IconColor::default(), + color: TextColor::default(), size: IconSize::default(), } } - pub fn color(mut self, color: IconColor) -> Self { + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index b20cd31036..b719a05b92 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,4 +1,4 @@ -use crate::{h_stack, prelude::*, ClickHandler, Icon, IconColor, IconElement, TextTooltip}; +use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement, TextColor, TextTooltip}; use gpui::{MouseButton, VisualContext}; use std::sync::Arc; @@ -16,7 +16,7 @@ impl Default for IconButtonHandlers { pub struct IconButton { id: ElementId, icon: Icon, - color: IconColor, + color: TextColor, variant: ButtonVariant, state: InteractionState, tooltip: Option, @@ -28,7 +28,7 @@ impl IconButton { Self { id: id.into(), icon, - color: IconColor::default(), + color: TextColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), tooltip: None, @@ -41,7 +41,7 @@ impl IconButton { self } - pub fn color(mut self, color: IconColor) -> Self { + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } @@ -71,7 +71,7 @@ impl IconButton { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let icon_color = match (self.state, self.color) { - (InteractionState::Disabled, _) => IconColor::Disabled, + (InteractionState::Disabled, _) => TextColor::Disabled, _ => self.color, }; diff --git a/crates/ui2/src/components/input.rs b/crates/ui2/src/components/input.rs index 1a44827fe8..9bcf5e4dba 100644 --- a/crates/ui2/src/components/input.rs +++ b/crates/ui2/src/components/input.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use crate::Label; -use crate::LabelColor; +use crate::TextColor; #[derive(Default, PartialEq)] pub enum InputVariant { @@ -71,15 +71,15 @@ impl Input { }; let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled { - LabelColor::Disabled + TextColor::Disabled } else { - LabelColor::Placeholder + TextColor::Placeholder }); let label = Label::new(self.value.clone()).color(if self.disabled { - LabelColor::Disabled + TextColor::Disabled } else { - LabelColor::Default + TextColor::Default }); div() diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index c316a07483..cbb75278c2 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -11,7 +11,7 @@ pub enum LabelSize { } #[derive(Default, PartialEq, Copy, Clone)] -pub enum LabelColor { +pub enum TextColor { #[default] Default, Accent, @@ -30,24 +30,24 @@ pub enum LabelColor { Warning, } -impl LabelColor { - pub fn hsla(&self, cx: &WindowContext) -> Hsla { +impl TextColor { + pub fn color(&self, cx: &WindowContext) -> Hsla { match self { - LabelColor::Default => cx.theme().colors().text, - LabelColor::Muted => cx.theme().colors().text_muted, - LabelColor::Created => cx.theme().status().created, - LabelColor::Modified => cx.theme().status().modified, - LabelColor::Deleted => cx.theme().status().deleted, - LabelColor::Disabled => cx.theme().colors().text_disabled, - LabelColor::Hidden => cx.theme().status().hidden, - LabelColor::Info => cx.theme().status().info, - LabelColor::Placeholder => cx.theme().colors().text_placeholder, - LabelColor::Accent => cx.theme().colors().text_accent, - LabelColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, - LabelColor::Error => cx.theme().status().error, - LabelColor::Selected => cx.theme().colors().text_accent, - LabelColor::Success => cx.theme().status().success, - LabelColor::Warning => cx.theme().status().warning, + TextColor::Default => cx.theme().colors().text, + TextColor::Muted => cx.theme().colors().text_muted, + TextColor::Created => cx.theme().status().created, + TextColor::Modified => cx.theme().status().modified, + TextColor::Deleted => cx.theme().status().deleted, + TextColor::Disabled => cx.theme().colors().text_disabled, + TextColor::Hidden => cx.theme().status().hidden, + TextColor::Info => cx.theme().status().info, + TextColor::Placeholder => cx.theme().colors().text_placeholder, + TextColor::Accent => cx.theme().colors().text_accent, + TextColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, + TextColor::Error => cx.theme().status().error, + TextColor::Selected => cx.theme().colors().text_accent, + TextColor::Success => cx.theme().status().success, + TextColor::Warning => cx.theme().status().warning, } } } @@ -65,7 +65,7 @@ pub struct Label { label: SharedString, size: LabelSize, line_height_style: LineHeightStyle, - color: LabelColor, + color: TextColor, strikethrough: bool, } @@ -75,7 +75,7 @@ impl Label { label: label.into(), size: LabelSize::Default, line_height_style: LineHeightStyle::default(), - color: LabelColor::Default, + color: TextColor::Default, strikethrough: false, } } @@ -85,7 +85,7 @@ impl Label { self } - pub fn color(mut self, color: LabelColor) -> Self { + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } @@ -109,7 +109,7 @@ impl Label { .top_1_2() .w_full() .h_px() - .bg(LabelColor::Hidden.hsla(cx)), + .bg(TextColor::Hidden.color(cx)), ) }) .map(|this| match self.size { @@ -119,7 +119,7 @@ impl Label { .when(self.line_height_style == LineHeightStyle::UILabel, |this| { this.line_height(relative(1.)) }) - .text_color(self.color.hsla(cx)) + .text_color(self.color.color(cx)) .child(self.label.clone()) } } @@ -128,7 +128,7 @@ impl Label { pub struct HighlightedLabel { label: SharedString, size: LabelSize, - color: LabelColor, + color: TextColor, highlight_indices: Vec, strikethrough: bool, } @@ -140,7 +140,7 @@ impl HighlightedLabel { Self { label: label.into(), size: LabelSize::Default, - color: LabelColor::Default, + color: TextColor::Default, highlight_indices, strikethrough: false, } @@ -151,7 +151,7 @@ impl HighlightedLabel { self } - pub fn color(mut self, color: LabelColor) -> Self { + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } @@ -170,7 +170,7 @@ impl HighlightedLabel { let mut runs: Vec = Vec::new(); for (char_ix, char) in self.label.char_indices() { - let mut color = self.color.hsla(cx); + let mut color = self.color.color(cx); if let Some(highlight_ix) = highlight_indices.peek() { if char_ix == *highlight_ix { @@ -207,7 +207,7 @@ impl HighlightedLabel { .my_auto() .w_full() .h_px() - .bg(LabelColor::Hidden.hsla(cx)), + .bg(TextColor::Hidden.color(cx)), ) }) .map(|this| match self.size { diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 5c42975b17..1ddad269dd 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,11 +1,11 @@ use gpui::div; +use crate::prelude::*; use crate::settings::user_settings; use crate::{ - disclosure_control, h_stack, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, - LabelColor, Toggle, + disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label, + TextColor, Toggle, }; -use crate::{prelude::*, GraphicSlot}; #[derive(Clone, Copy, Default, Debug, PartialEq)] pub enum ListItemVariant { @@ -68,7 +68,7 @@ impl ListHeader { .items_center() .children(icons.into_iter().map(|i| { IconElement::new(i) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small) })), ), @@ -106,10 +106,10 @@ impl ListHeader { .items_center() .children(self.left_icon.map(|i| { IconElement::new(i) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small) })) - .child(Label::new(self.label.clone()).color(LabelColor::Muted)), + .child(Label::new(self.label.clone()).color(TextColor::Muted)), ) .child(disclosure_control), ) @@ -157,10 +157,10 @@ impl ListSubHeader { .items_center() .children(self.left_icon.map(|i| { IconElement::new(i) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small) })) - .child(Label::new(self.label.clone()).color(LabelColor::Muted)), + .child(Label::new(self.label.clone()).color(TextColor::Muted)), ), ) } @@ -291,7 +291,7 @@ impl ListEntry { h_stack().child( IconElement::new(i) .size(IconSize::Small) - .color(IconColor::Muted), + .color(TextColor::Muted), ), ), Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))), @@ -394,7 +394,7 @@ impl List { (false, _) => div().children(self.items), (true, Toggle::Toggled(false)) => div(), (true, _) => { - div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted)) + div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted)) } }; diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index d73b15940e..f1b50bb56d 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor}; +use crate::{h_stack, v_stack, KeyBinding, Label, TextColor}; #[derive(Component)] pub struct Palette { @@ -54,7 +54,7 @@ impl Palette { v_stack() .gap_px() .child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child( - Label::new(self.input_placeholder.clone()).color(LabelColor::Placeholder), + Label::new(self.input_placeholder.clone()).color(TextColor::Placeholder), ))) .child( div() @@ -75,7 +75,7 @@ impl Palette { Some( h_stack().justify_between().px_2().py_1().child( Label::new(self.empty_string.clone()) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), ) } else { diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index e936dc924a..7128257628 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::{Icon, IconColor, IconElement, Label, LabelColor}; +use crate::{Icon, IconElement, Label, TextColor}; use gpui::{red, Div, ElementId, Render, View, VisualContext}; #[derive(Component, Clone)] @@ -92,20 +92,18 @@ impl Tab { let label = match (self.git_status, is_deleted) { (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone()) - .color(LabelColor::Hidden) + .color(TextColor::Hidden) .set_strikethrough(true), (GitStatus::None, false) => Label::new(self.title.clone()), - (GitStatus::Created, false) => { - Label::new(self.title.clone()).color(LabelColor::Created) - } + (GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created), (GitStatus::Modified, false) => { - Label::new(self.title.clone()).color(LabelColor::Modified) + Label::new(self.title.clone()).color(TextColor::Modified) } - (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent), + (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent), (GitStatus::Conflict, false) => Label::new(self.title.clone()), }; - let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted); + let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted); let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current { false => ( @@ -148,7 +146,7 @@ impl Tab { .children(has_fs_conflict.then(|| { IconElement::new(Icon::ExclamationTriangle) .size(crate::IconSize::Small) - .color(IconColor::Warning) + .color(TextColor::Warning) })) .children(self.icon.map(IconElement::new)) .children(if self.close_side == IconSide::Left { diff --git a/crates/ui2/src/components/toggle.rs b/crates/ui2/src/components/toggle.rs index 368c95662f..1683773e16 100644 --- a/crates/ui2/src/components/toggle.rs +++ b/crates/ui2/src/components/toggle.rs @@ -1,6 +1,6 @@ use gpui::{div, Component, ParentElement}; -use crate::{Icon, IconColor, IconElement, IconSize}; +use crate::{Icon, IconElement, IconSize, TextColor}; /// Whether the entry is toggleable, and if so, whether it is currently toggled. /// @@ -49,12 +49,12 @@ pub fn disclosure_control(toggle: Toggle) -> impl Component { (false, _) => div(), (_, true) => div().child( IconElement::new(Icon::ChevronDown) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small), ), (_, false) => div().child( IconElement::new(Icon::ChevronRight) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small), ), } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 536bb22ba0..58375b0b67 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,8 +1,8 @@ use gpui::{Div, Render}; use theme2::ActiveTheme; -use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor, StyledExt}; -use crate::{prelude::*, LabelSize}; +use crate::prelude::*; +use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor}; pub struct TextTooltip { title: SharedString, @@ -52,7 +52,7 @@ impl Render for TextTooltip { this.child( Label::new(meta) .size(LabelSize::Small) - .color(LabelColor::Muted), + .color(TextColor::Muted), ) }) } diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 545f437a9b..7368118f96 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -6,8 +6,8 @@ pub use gpui::{ }; pub use crate::elevation::*; -pub use crate::ButtonVariant; pub use crate::StyledExt; +pub use crate::{ButtonVariant, TextColor}; pub use theme2::ActiveTheme; use gpui::Hsla; diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index 4615adbfa4..bb81d6230f 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -10,9 +10,9 @@ use theme2::ActiveTheme; use crate::{binding, HighlightedText}; use crate::{ Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus, - HighlightedLine, Icon, KeyBinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, - MicStatus, Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, - PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, + HighlightedLine, Icon, KeyBinding, Label, ListEntry, ListEntrySize, Livestream, MicStatus, + Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, PublicPlayer, + ScreenShareStatus, Symbol, Tab, TextColor, Toggle, VideoStatus, }; use crate::{ListItem, NotificationAction}; @@ -490,20 +490,20 @@ pub fn static_project_panel_project_items() -> Vec { ListEntry::new(Label::new(".config")) .left_icon(Icon::Folder.into()) .indent_level(1), - ListEntry::new(Label::new(".git").color(LabelColor::Hidden)) + ListEntry::new(Label::new(".git").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new(".cargo")) .left_icon(Icon::Folder.into()) .indent_level(1), - ListEntry::new(Label::new(".idea").color(LabelColor::Hidden)) + ListEntry::new(Label::new(".idea").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new("assets")) .left_icon(Icon::Folder.into()) .indent_level(1) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden)) + ListEntry::new(Label::new("cargo-target").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new("crates")) @@ -528,7 +528,7 @@ pub fn static_project_panel_project_items() -> Vec { ListEntry::new(Label::new("call")) .left_icon(Icon::Folder.into()) .indent_level(2), - ListEntry::new(Label::new("sqlez").color(LabelColor::Modified)) + ListEntry::new(Label::new("sqlez").color(TextColor::Modified)) .left_icon(Icon::Folder.into()) .indent_level(2) .toggle(Toggle::Toggled(false)), @@ -543,45 +543,45 @@ pub fn static_project_panel_project_items() -> Vec { ListEntry::new(Label::new("derive_element.rs")) .left_icon(Icon::FileRust.into()) .indent_level(4), - ListEntry::new(Label::new("storybook").color(LabelColor::Modified)) + ListEntry::new(Label::new("storybook").color(TextColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(1) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("docs").color(LabelColor::Default)) + ListEntry::new(Label::new("docs").color(TextColor::Default)) .left_icon(Icon::Folder.into()) .indent_level(2) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("src").color(LabelColor::Modified)) + ListEntry::new(Label::new("src").color(TextColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(3) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("ui").color(LabelColor::Modified)) + ListEntry::new(Label::new("ui").color(TextColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(4) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("component").color(LabelColor::Created)) + ListEntry::new(Label::new("component").color(TextColor::Created)) .left_icon(Icon::FolderOpen.into()) .indent_level(5) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default)) + ListEntry::new(Label::new("facepile.rs").color(TextColor::Default)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default)) + ListEntry::new(Label::new("follow_group.rs").color(TextColor::Default)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created)) + ListEntry::new(Label::new("list_item.rs").color(TextColor::Created)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("tab.rs").color(LabelColor::Default)) + ListEntry::new(Label::new("tab.rs").color(TextColor::Default)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("target").color(LabelColor::Hidden)) + ListEntry::new(Label::new("target").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new(".dockerignore")) .left_icon(Icon::FileGeneric.into()) .indent_level(1), - ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden)) + ListEntry::new(Label::new(".DS_Store").color(TextColor::Hidden)) .left_icon(Icon::FileGeneric.into()) .indent_level(1), ListEntry::new(Label::new("Cargo.lock")) diff --git a/crates/ui2/src/to_extract/buffer_search.rs b/crates/ui2/src/to_extract/buffer_search.rs index 9993cd3612..996ac6d253 100644 --- a/crates/ui2/src/to_extract/buffer_search.rs +++ b/crates/ui2/src/to_extract/buffer_search.rs @@ -1,7 +1,7 @@ use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; -use crate::{h_stack, Icon, IconButton, IconColor, Input}; +use crate::{h_stack, Icon, IconButton, Input, TextColor}; #[derive(Clone)] pub struct BufferSearch { @@ -36,7 +36,7 @@ impl Render for BufferSearch { .child( h_stack().child(Input::new("Search")).child( IconButton::::new("replace", Icon::Replace) - .when(self.is_replace_open, |this| this.color(IconColor::Accent)) + .when(self.is_replace_open, |this| this.color(TextColor::Accent)) .on_click(|buffer_search, cx| { buffer_search.toggle_replace(cx); }), diff --git a/crates/ui2/src/to_extract/chat_panel.rs b/crates/ui2/src/to_extract/chat_panel.rs index 538b5dfceb..b1d208fd67 100644 --- a/crates/ui2/src/to_extract/chat_panel.rs +++ b/crates/ui2/src/to_extract/chat_panel.rs @@ -1,7 +1,7 @@ use chrono::NaiveDateTime; use crate::prelude::*; -use crate::{Icon, IconButton, Input, Label, LabelColor}; +use crate::{Icon, IconButton, Input, Label, TextColor}; #[derive(Component)] pub struct ChatPanel { @@ -95,7 +95,7 @@ impl ChatMessage { .child(Label::new(self.author.clone())) .child( Label::new(self.sent_at.format("%m/%d/%Y").to_string()) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), ) .child(div().child(Label::new(self.text.clone()))) diff --git a/crates/ui2/src/to_extract/copilot.rs b/crates/ui2/src/to_extract/copilot.rs index 8750ab3c51..c5622f5be6 100644 --- a/crates/ui2/src/to_extract/copilot.rs +++ b/crates/ui2/src/to_extract/copilot.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, Button, Label, LabelColor, Modal}; +use crate::{prelude::*, Button, Label, Modal, TextColor}; #[derive(Component)] pub struct CopilotModal { @@ -14,7 +14,7 @@ impl CopilotModal { div().id(self.id.clone()).child( Modal::new("some-id") .title("Connect Copilot to Zed") - .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(LabelColor::Muted)) + .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(TextColor::Muted)) .primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)), ) } diff --git a/crates/ui2/src/to_extract/editor_pane.rs b/crates/ui2/src/to_extract/editor_pane.rs index fd21e81242..f03323f93f 100644 --- a/crates/ui2/src/to_extract/editor_pane.rs +++ b/crates/ui2/src/to_extract/editor_pane.rs @@ -5,7 +5,7 @@ use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; use crate::{ hello_world_rust_editor_with_status_example, v_stack, Breadcrumb, Buffer, BufferSearch, Icon, - IconButton, IconColor, Symbol, Tab, TabBar, Toolbar, + IconButton, Symbol, Tab, TabBar, TextColor, Toolbar, }; #[derive(Clone)] @@ -63,7 +63,7 @@ impl Render for EditorPane { IconButton::new("toggle_inlay_hints", Icon::InlayHint), IconButton::::new("buffer_search", Icon::MagnifyingGlass) .when(self.is_buffer_search_open, |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|editor, cx| { editor.toggle_buffer_search(cx); diff --git a/crates/ui2/src/to_extract/notifications_panel.rs b/crates/ui2/src/to_extract/notifications_panel.rs index b2cc4a7846..98e1179851 100644 --- a/crates/ui2/src/to_extract/notifications_panel.rs +++ b/crates/ui2/src/to_extract/notifications_panel.rs @@ -1,8 +1,8 @@ use crate::utils::naive_format_distance_from_now; use crate::{ h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, ButtonOrIconButton, - Icon, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator, - PublicPlayer, UnreadIndicator, + Icon, IconElement, Label, LineHeightStyle, ListHeaderMeta, ListSeparator, PublicPlayer, + TextColor, UnreadIndicator, }; use crate::{ClickHandler, ListHeader}; @@ -48,7 +48,7 @@ impl NotificationsPanel { .border_color(cx.theme().colors().border_variant) .child( Label::new("Search...") - .color(LabelColor::Placeholder) + .color(TextColor::Placeholder) .line_height_style(LineHeightStyle::UILabel), ), ) @@ -252,7 +252,7 @@ impl Notification { if let Some(icon) = icon { meta_el = meta_el.child(IconElement::new(icon.clone())); } - meta_el.child(Label::new(text.clone()).color(LabelColor::Muted)) + meta_el.child(Label::new(text.clone()).color(TextColor::Muted)) }) .collect::>(), ) @@ -311,7 +311,7 @@ impl Notification { true, true, )) - .color(LabelColor::Muted), + .color(TextColor::Muted), ) .child(self.render_meta_items(cx)), ) @@ -321,11 +321,11 @@ impl Notification { // Show the taken_message (Some(_), Some(action_taken)) => h_stack() .children(action_taken.taken_message.0.map(|icon| { - IconElement::new(icon).color(crate::IconColor::Muted) + IconElement::new(icon).color(crate::TextColor::Muted) })) .child( Label::new(action_taken.taken_message.1.clone()) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), // Show the actions (Some(actions), None) => { diff --git a/crates/ui2/src/to_extract/status_bar.rs b/crates/ui2/src/to_extract/status_bar.rs index 34a5993e69..bc236ea1fa 100644 --- a/crates/ui2/src/to_extract/status_bar.rs +++ b/crates/ui2/src/to_extract/status_bar.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::prelude::*; -use crate::{Button, Icon, IconButton, IconColor, ToolDivider, Workspace}; +use crate::{Button, Icon, IconButton, TextColor, ToolDivider, Workspace}; #[derive(Default, PartialEq)] pub enum Tool { @@ -110,7 +110,7 @@ impl StatusBar { .child( IconButton::::new("project_panel", Icon::FileTree) .when(workspace.is_project_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|workspace, cx| { workspace.toggle_project_panel(cx); @@ -119,7 +119,7 @@ impl StatusBar { .child( IconButton::::new("collab_panel", Icon::Hash) .when(workspace.is_collab_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|workspace, cx| { workspace.toggle_collab_panel(); @@ -174,7 +174,7 @@ impl StatusBar { .child( IconButton::::new("terminal", Icon::Terminal) .when(workspace.is_terminal_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|workspace, cx| { workspace.toggle_terminal(cx); @@ -183,7 +183,7 @@ impl StatusBar { .child( IconButton::::new("chat_panel", Icon::MessageBubbles) .when(workspace.is_chat_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|workspace, cx| { workspace.toggle_chat_panel(cx); @@ -192,7 +192,7 @@ impl StatusBar { .child( IconButton::::new("assistant_panel", Icon::Ai) .when(workspace.is_assistant_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|workspace, cx| { workspace.toggle_assistant_panel(cx); diff --git a/crates/ui2/src/to_extract/title_bar.rs b/crates/ui2/src/to_extract/title_bar.rs index 87d7dd4146..9aa8777a9d 100644 --- a/crates/ui2/src/to_extract/title_bar.rs +++ b/crates/ui2/src/to_extract/title_bar.rs @@ -6,8 +6,8 @@ use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; use crate::settings::user_settings; use crate::{ - Avatar, Button, Icon, IconButton, IconColor, MicStatus, PlayerStack, PlayerWithCallStatus, - ScreenShareStatus, ToolDivider, TrafficLights, + Avatar, Button, Icon, IconButton, MicStatus, PlayerStack, PlayerWithCallStatus, + ScreenShareStatus, TextColor, ToolDivider, TrafficLights, }; #[derive(Clone)] @@ -152,19 +152,19 @@ impl Render for TitleBar { .gap_1() .child( IconButton::::new("toggle_mic_status", Icon::Mic) - .when(self.is_mic_muted(), |this| this.color(IconColor::Error)) + .when(self.is_mic_muted(), |this| this.color(TextColor::Error)) .on_click(|title_bar, cx| title_bar.toggle_mic_status(cx)), ) .child( IconButton::::new("toggle_deafened", Icon::AudioOn) - .when(self.is_deafened, |this| this.color(IconColor::Error)) + .when(self.is_deafened, |this| this.color(TextColor::Error)) .on_click(|title_bar, cx| title_bar.toggle_deafened(cx)), ) .child( IconButton::::new("toggle_screen_share", Icon::Screen) .when( self.screen_share_status == ScreenShareStatus::Shared, - |this| this.color(IconColor::Accent), + |this| this.color(TextColor::Accent), ) .on_click(|title_bar, cx| { title_bar.toggle_screen_share_status(cx) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index e3ea4863c9..05dc83673f 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -27,7 +27,7 @@ use std::{ }, }; use ui::v_stack; -use ui::{prelude::*, Icon, IconButton, IconColor, IconElement, TextTooltip}; +use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, TextTooltip}; use util::truncate_and_remove_front; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] @@ -1432,13 +1432,13 @@ impl Pane { Some( IconElement::new(Icon::ExclamationTriangle) .size(ui::IconSize::Small) - .color(IconColor::Warning), + .color(TextColor::Warning), ) } else if item.is_dirty(cx) { Some( IconElement::new(Icon::ExclamationTriangle) .size(ui::IconSize::Small) - .color(IconColor::Info), + .color(TextColor::Info), ) } else { None diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 88e8dc7934..4fa09e5b02 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -69,7 +69,7 @@ use std::{ }; use theme2::ActiveTheme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, LabelColor, TextTooltip}; +use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, TextTooltip}; use util::ResultExt; use uuid::Uuid; use workspace_settings::{AutosaveSetting, WorkspaceSettings}; @@ -2477,7 +2477,7 @@ impl Workspace { .child( Button::new("player") .variant(ButtonVariant::Ghost) - .color(Some(LabelColor::Player(0))), + .color(Some(TextColor::Player(0))), ) .tooltip(move |_, cx| { cx.build_view(|cx| TextTooltip::new("Toggle following")) @@ -2499,7 +2499,7 @@ impl Workspace { .child( Button::new("branch_name") .variant(ButtonVariant::Ghost) - .color(Some(LabelColor::Muted)), + .color(Some(TextColor::Muted)), ) .tooltip(move |_, cx| { // todo!() Replace with real action. From 3419aaf17ec7385c1495f965f12ea91e78816e49 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 11:42:58 -0800 Subject: [PATCH 105/126] Fix several shutdown related bugs --- crates/gpui2/src/app.rs | 21 +++++++++++++++----- crates/gpui2/src/app/entity_map.rs | 2 +- crates/gpui2/src/app/test_context.rs | 2 +- crates/live_kit_client2/examples/test_app.rs | 2 +- crates/zed2/src/zed2.rs | 1 - 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 5463550587..6152fae9d2 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -234,10 +234,10 @@ impl AppContext { app_version: platform.app_version().ok(), }; - Rc::new_cyclic(|this| AppCell { + let app = Rc::new_cyclic(|this| AppCell { app: RefCell::new(AppContext { this: this.clone(), - platform, + platform: platform.clone(), app_metadata, text_system, flushing_effects: false, @@ -269,12 +269,21 @@ impl AppContext { layout_id_buffer: Default::default(), propagate_event: true, }), - }) + }); + + platform.on_quit(Box::new({ + let cx = app.clone(); + move || { + cx.borrow_mut().shutdown(); + } + })); + + app } /// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit` /// will be given 100ms to complete before exiting. - pub fn quit(&mut self) { + pub fn shutdown(&mut self) { let mut futures = Vec::new(); for observer in self.quit_observers.remove(&()) { @@ -292,8 +301,10 @@ impl AppContext { { log::error!("timed out waiting on app_will_quit"); } + } - self.globals_by_type.clear(); + pub fn quit(&mut self) { + self.platform.quit(); } pub fn app_metadata(&self) -> AppMetadata { diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index 1ae9aec9b5..1e01921cd4 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -26,7 +26,7 @@ impl EntityId { impl Display for EntityId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self) + write!(f, "{}", self.as_u64()) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 44c31bbd69..51c564fdd8 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -96,7 +96,7 @@ impl TestAppContext { } pub fn quit(&self) { - self.app.borrow_mut().quit(); + self.app.borrow_mut().shutdown(); } pub fn refresh(&mut self) -> Result<()> { diff --git a/crates/live_kit_client2/examples/test_app.rs b/crates/live_kit_client2/examples/test_app.rs index 98302eb35c..0b9e54f9b0 100644 --- a/crates/live_kit_client2/examples/test_app.rs +++ b/crates/live_kit_client2/examples/test_app.rs @@ -167,7 +167,7 @@ fn main() { panic!("unexpected message"); } - cx.update(|cx| cx.quit()).ok(); + cx.update(|cx| cx.shutdown()).ok(); }) .detach(); }); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 73faeaaaf4..2f7a38b041 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -502,7 +502,6 @@ fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext) { cx.update(|_, cx| { cx.quit(); })?; - anyhow::Ok(()) }) .detach_and_log_err(cx); From 59ec9e508bb17c8b1897c26a5e11216f0d1a787c Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 14 Nov 2023 14:47:00 -0500 Subject: [PATCH 106/126] Avoid user NPM config/cache & put NodeRuntime installation behind a lock --- crates/node_runtime/src/node_runtime.rs | 29 ++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index a099a025e6..2621c58120 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use serde::Deserialize; -use smol::{fs, io::BufReader, process::Command}; +use smol::{fs, io::BufReader, lock::Mutex, process::Command}; use std::process::{Output, Stdio}; use std::{ env::consts, @@ -45,14 +45,19 @@ pub trait NodeRuntime: Send + Sync { pub struct RealNodeRuntime { http: Arc, + installation_lock: Mutex<()>, } impl RealNodeRuntime { pub fn new(http: Arc) -> Arc { - Arc::new(RealNodeRuntime { http }) + Arc::new(RealNodeRuntime { + http, + installation_lock: Mutex::new(()), + }) } async fn install_if_needed(&self) -> Result { + let _lock = self.installation_lock.lock().await; log::info!("Node runtime install_if_needed"); let arch = match consts::ARCH { @@ -73,6 +78,9 @@ impl RealNodeRuntime { .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) + .args(["--cache".into(), node_dir.join("cache")]) + .args(["--userconfig".into(), node_dir.join("blank_user_npmrc")]) + .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]) .status() .await; let valid = matches!(result, Ok(status) if status.success()); @@ -96,6 +104,11 @@ impl RealNodeRuntime { archive.unpack(&node_containing_dir).await?; } + // Note: Not in the `if !valid {}` so we can populate these for existing installations + _ = fs::create_dir(node_dir.join("cache")).await; + _ = fs::write(node_dir.join("blank_user_npmrc"), []).await; + _ = fs::write(node_dir.join("blank_global_npmrc"), []).await; + anyhow::Ok(node_dir) } } @@ -137,7 +150,17 @@ impl NodeRuntime for RealNodeRuntime { let mut command = Command::new(node_binary); command.env("PATH", env_path); - command.arg(npm_file).arg(subcommand).args(args); + command.arg(npm_file).arg(subcommand); + command.args(["--cache".into(), installation_path.join("cache")]); + command.args([ + "--userconfig".into(), + installation_path.join("blank_user_npmrc"), + ]); + command.args([ + "--globalconfig".into(), + installation_path.join("blank_global_npmrc"), + ]); + command.args(args); if let Some(directory) = directory { command.current_dir(directory); From 62fc0b21006146adfd624c433c08841611e77a35 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 12:06:41 -0800 Subject: [PATCH 107/126] Remove unnescessary unimplemented --- crates/gpui2/src/platform/test/platform.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 4afcc4fc1a..79a80a3d51 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -46,9 +46,7 @@ impl Platform for TestPlatform { unimplemented!() } - fn quit(&self) { - unimplemented!() - } + fn quit(&self) {} fn restart(&self) { unimplemented!() @@ -141,9 +139,7 @@ impl Platform for TestPlatform { unimplemented!() } - fn on_quit(&self, _callback: Box) { - unimplemented!() - } + fn on_quit(&self, _callback: Box) {} fn on_reopen(&self, _callback: Box) { unimplemented!() From 3a4c5aa44087af95dc34ec3e536c6fd825a79b14 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 13:34:14 -0700 Subject: [PATCH 108/126] Implement FileFinder --- crates/file_finder2/src/file_finder.rs | 3121 ++++++++++++------------ crates/picker2/src/picker2.rs | 5 + crates/workspace2/src/workspace2.rs | 144 +- 3 files changed, 1615 insertions(+), 1655 deletions(-) diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 67fb1e400f..13296887cb 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,7 +2,8 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, AppContext, Div, EventEmitter, Render, Task, View, ViewContext, WindowContext, + actions, div, AppContext, Component, Div, EventEmitter, Model, ParentElement, Render, + StatelessInteractive, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; @@ -14,7 +15,9 @@ use std::{ }, }; use text::Point; -use util::{paths::PathLikeWithPosition, post_inc}; +use theme::ActiveTheme; +use ui::{v_stack, HighlightedLabel, StyledExt}; +use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::{Modal, ModalEvent, Workspace}; actions!(Toggle); @@ -24,14 +27,16 @@ pub struct FileFinder { } pub fn init(cx: &mut AppContext) { - cx.observe_new_views(FileFinder::register); + cx.observe_new_views(FileFinder::register).detach(); } impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { + dbg!("yay"); workspace.register_action(|workspace, _: &Toggle, cx| { + dbg!("yayer"); let Some(file_finder) = workspace.current_modal::(cx) else { - workspace.toggle_modal(cx, |cx| FileFinder::new(workspace, cx)); + Self::open(workspace, cx); return; }; file_finder.update(cx, |file_finder, cx| { @@ -42,7 +47,7 @@ impl FileFinder { }); } - fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> Self { + fn open(workspace: &mut Workspace, cx: &mut ViewContext) { let project = workspace.project().read(cx); let currently_opened_path = workspace @@ -84,20 +89,25 @@ impl FileFinder { .collect(); let project = workspace.project().clone(); - let workspace = cx.handle().downgrade(); - let finder = cx.add_view(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace, - project, - currently_opened_path, - history_items, - cx, - ), + let weak_workspace = cx.view().downgrade(); + workspace.toggle_modal(cx, |cx| { + let delegate = FileFinderDelegate::new( + cx.view().downgrade(), + weak_workspace, + project, + currently_opened_path, + history_items, cx, - ) + ); + + FileFinder::new(delegate, cx) }); - finder + } + + fn new(delegate: FileFinderDelegate, cx: &mut ViewContext) -> Self { + Self { + picker: cx.build_view(|cx| Picker::new(delegate, cx)), + } } } @@ -116,8 +126,9 @@ impl Render for FileFinder { } pub struct FileFinderDelegate { - workspace: WeakViewHandle, - project: ModelHandle, + file_finder: WeakView, + workspace: WeakView, + project: Model, search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, @@ -263,82 +274,6 @@ impl FoundPath { const MAX_RECENT_SELECTIONS: usize = 20; -fn toggle_or_cycle_file_finder( - workspace: &mut Workspace, - _: &Toggle, - cx: &mut ViewContext, -) { - match workspace.modal::() { - Some(file_finder) => file_finder.update(cx, |file_finder, cx| { - let current_index = file_finder.delegate().selected_index(); - file_finder.select_next(&menu::SelectNext, cx); - let new_index = file_finder.delegate().selected_index(); - if current_index == new_index { - file_finder.select_first(&menu::SelectFirst, cx); - } - }), - None => { - workspace.toggle_modal(cx, |workspace, cx| { - let project = workspace.project().read(cx); - - let currently_opened_path = workspace - .active_item(cx) - .and_then(|item| item.project_path(cx)) - .map(|project_path| { - let abs_path = project - .worktree_for_id(project_path.worktree_id, cx) - .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)); - FoundPath::new(project_path, abs_path) - }); - - // if exists, bubble the currently opened path to the top - let history_items = currently_opened_path - .clone() - .into_iter() - .chain( - workspace - .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx) - .into_iter() - .filter(|(history_path, _)| { - Some(history_path) - != currently_opened_path - .as_ref() - .map(|found_path| &found_path.project) - }) - .filter(|(_, history_abs_path)| { - history_abs_path.as_ref() - != currently_opened_path - .as_ref() - .and_then(|found_path| found_path.absolute.as_ref()) - }) - .filter(|(_, history_abs_path)| match history_abs_path { - Some(abs_path) => history_file_exists(abs_path), - None => true, - }) - .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)), - ) - .collect(); - - let project = workspace.project().clone(); - let workspace = cx.handle().downgrade(); - let finder = cx.add_view(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace, - project, - currently_opened_path, - history_items, - cx, - ), - cx, - ) - }); - finder - }); - } - } -} - #[cfg(not(test))] fn history_file_exists(abs_path: &PathBuf) -> bool { abs_path.exists() @@ -371,17 +306,23 @@ impl FileSearchQuery { impl FileFinderDelegate { fn new( - workspace: WeakViewHandle, - project: ModelHandle, + file_finder: WeakView, + workspace: WeakView, + project: Model, currently_opened_path: Option, history_items: Vec, cx: &mut ViewContext, ) -> Self { - cx.observe(&project, |picker, _, cx| { - picker.update_matches(picker.query(cx), cx); + cx.observe(&project, |file_finder, _, cx| { + //todo!() We should probably not re-render on every project anything + file_finder + .picker + .update(cx, |picker, cx| picker.refresh(cx)) }) .detach(); + Self { + file_finder, workspace, project, search_count: 0, @@ -399,7 +340,7 @@ impl FileFinderDelegate { fn spawn_search( &mut self, query: PathLikeWithPosition, - cx: &mut ViewContext, + cx: &mut ViewContext>, ) -> Task<()> { let relative_to = self .currently_opened_path @@ -437,14 +378,14 @@ impl FileFinderDelegate { false, 100, &cancel_flag, - cx.background(), + cx.background_executor().clone(), ) .await; let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); picker .update(&mut cx, |picker, cx| { picker - .delegate_mut() + .delegate .set_search_matches(search_id, did_cancel, query, matches, cx) }) .log_err(); @@ -457,7 +398,7 @@ impl FileFinderDelegate { did_cancel: bool, query: PathLikeWithPosition, matches: Vec, - cx: &mut ViewContext, + cx: &mut ViewContext>, ) { if search_id >= self.latest_search_id { self.latest_search_id = search_id; @@ -589,6 +530,8 @@ impl FileFinderDelegate { } impl PickerDelegate for FileFinderDelegate { + type ListItem = Div>; + fn placeholder_text(&self) -> Arc { "Search project files...".into() } @@ -601,12 +544,16 @@ impl PickerDelegate for FileFinderDelegate { self.selected_index.unwrap_or(0) } - fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { self.selected_index = Some(ix); cx.notify(); } - fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext) -> Task<()> { + fn update_matches( + &mut self, + raw_query: String, + cx: &mut ViewContext>, + ) -> Task<()> { if raw_query.is_empty() { let project = self.project.read(cx); self.latest_search_id = post_inc(&mut self.search_count); @@ -644,9 +591,9 @@ impl PickerDelegate for FileFinderDelegate { } } - fn confirm(&mut self, secondary: bool, cx: &mut ViewContext) { + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { if let Some(m) = self.matches.get(self.selected_index()) { - if let Some(workspace) = self.workspace.upgrade(cx) { + if let Some(workspace) = self.workspace.upgrade() { let open_task = workspace.update(cx, move |workspace, cx| { let split_or_open = |workspace: &mut Workspace, project_path, cx| { if secondary { @@ -722,6 +669,8 @@ impl PickerDelegate for FileFinderDelegate { .and_then(|query| query.column) .unwrap_or(0) .saturating_sub(1); + let finder = self.file_finder.clone(); + cx.spawn(|_, mut cx| async move { let item = open_task.await.log_err()?; if let Some(row) = row { @@ -740,10 +689,9 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } - workspace - .downgrade() - .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx)) - .log_err(); + finder + .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .ok()?; Some(()) }) @@ -752,1490 +700,1497 @@ impl PickerDelegate for FileFinderDelegate { } } - fn dismissed(&mut self, _: &mut ViewContext) {} + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.file_finder + .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .log_err(); + } fn render_match( &self, ix: usize, - mouse_state: &mut MouseState, selected: bool, - cx: &AppContext, - ) -> AnyElement> { + cx: &mut ViewContext>, + ) -> Self::ListItem { let path_match = self .matches .get(ix) .expect("Invalid matches state: no element for index {ix}"); - let theme = theme::current(cx); - let style = theme.picker.item.in_state(selected).style_for(mouse_state); + let theme = cx.theme(); + let colors = theme.colors(); + let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match, cx, ix); - Flex::column() - .with_child( - Label::new(file_name, style.label.clone()).with_highlights(file_name_positions), + + div() + .px_1() + .text_color(colors.text) + .text_ui() + .bg(colors.ghost_element_background) + .rounded_md() + .when(selected, |this| this.bg(colors.ghost_element_selected)) + .hover(|this| this.bg(colors.ghost_element_hover)) + .child( + v_stack() + .child(HighlightedLabel::new(file_name, file_name_positions)) + .child(HighlightedLabel::new(full_path, full_path_positions)), ) - .with_child( - Label::new(full_path, style.label.clone()).with_highlights(full_path_positions), - ) - .flex(1., false) - .contained() - .with_style(style.container) - .into_any_named("match") } } -#[cfg(test)] -mod tests { - use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; - - use super::*; - use editor::Editor; - use gpui::{TestAppContext, ViewHandle}; - use menu::{Confirm, SelectNext}; - use serde_json::json; - use workspace::{AppState, Workspace}; - - #[ctor::ctor] - fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } - } - - #[gpui::test] - async fn test_matching_paths(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "a": { - "banana": "", - "bandana": "", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - cx.dispatch_action(window.into(), Toggle); - - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder.delegate_mut().update_matches("bna".to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - assert_eq!(finder.delegate().matches.len(), 2); - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.into(), SelectNext); - cx.dispatch_action(window.into(), Confirm); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - cx.read(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - assert_eq!( - active_item - .as_any() - .downcast_ref::() - .unwrap() - .read(cx) - .title(cx), - "bandana" - ); - }); - } - - #[gpui::test] - async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - let first_file_name = "first.rs"; - let first_file_contents = "// First Rust file"; - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - first_file_name: first_file_contents, - "second.rs": "// Second Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - cx.dispatch_action(window.into(), Toggle); - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - - let file_query = &first_file_name[..3]; - let file_row = 1; - let file_column = 3; - assert!(file_column <= first_file_contents.len()); - let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); - finder - .update(cx, |finder, cx| { - finder - .delegate_mut() - .update_matches(query_inside_file.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let finder = finder.delegate(); - assert_eq!(finder.matches.len(), 1); - let latest_search_query = finder - .latest_search_query - .as_ref() - .expect("Finder should have a query after the update_matches call"); - assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); - assert_eq!( - latest_search_query.path_like.file_query_end, - Some(file_query.len()) - ); - assert_eq!(latest_search_query.row, Some(file_row)); - assert_eq!(latest_search_query.column, Some(file_column as u32)); - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.into(), SelectNext); - cx.dispatch_action(window.into(), Confirm); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - let editor = cx.update(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - active_item.downcast::().unwrap() - }); - cx.foreground().advance_clock(Duration::from_secs(2)); - cx.foreground().start_waiting(); - cx.foreground().finish_waiting(); - editor.update(cx, |editor, cx| { - let all_selections = editor.selections.all_adjusted(cx); - assert_eq!( - all_selections.len(), - 1, - "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" - ); - let caret_selection = all_selections.into_iter().next().unwrap(); - assert_eq!(caret_selection.start, caret_selection.end, - "Caret selection should have its start and end at the same position"); - assert_eq!(file_row, caret_selection.start.row + 1, - "Query inside file should get caret with the same focus row"); - assert_eq!(file_column, caret_selection.start.column as usize + 1, - "Query inside file should get caret with the same focus column"); - }); - } - - #[gpui::test] - async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - let first_file_name = "first.rs"; - let first_file_contents = "// First Rust file"; - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - first_file_name: first_file_contents, - "second.rs": "// Second Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - cx.dispatch_action(window.into(), Toggle); - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - - let file_query = &first_file_name[..3]; - let file_row = 200; - let file_column = 300; - assert!(file_column > first_file_contents.len()); - let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); - finder - .update(cx, |finder, cx| { - finder - .delegate_mut() - .update_matches(query_outside_file.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let finder = finder.delegate(); - assert_eq!(finder.matches.len(), 1); - let latest_search_query = finder - .latest_search_query - .as_ref() - .expect("Finder should have a query after the update_matches call"); - assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); - assert_eq!( - latest_search_query.path_like.file_query_end, - Some(file_query.len()) - ); - assert_eq!(latest_search_query.row, Some(file_row)); - assert_eq!(latest_search_query.column, Some(file_column as u32)); - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.into(), SelectNext); - cx.dispatch_action(window.into(), Confirm); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - let editor = cx.update(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - active_item.downcast::().unwrap() - }); - cx.foreground().advance_clock(Duration::from_secs(2)); - cx.foreground().start_waiting(); - cx.foreground().finish_waiting(); - editor.update(cx, |editor, cx| { - let all_selections = editor.selections.all_adjusted(cx); - assert_eq!( - all_selections.len(), - 1, - "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" - ); - let caret_selection = all_selections.into_iter().next().unwrap(); - assert_eq!(caret_selection.start, caret_selection.end, - "Caret selection should have its start and end at the same position"); - assert_eq!(0, caret_selection.start.row, - "Excessive rows (as in query outside file borders) should get trimmed to last file row"); - assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, - "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); - }); - } - - #[gpui::test] - async fn test_matching_cancellation(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/dir", - json!({ - "hello": "", - "goodbye": "", - "halogen-light": "", - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - let finder = cx - .add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), - cx, - ), - cx, - ) - }) - .root(cx); - - let query = test_path_like("hi"); - finder - .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) - .await; - finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5)); - - finder.update(cx, |finder, cx| { - let delegate = finder.delegate_mut(); - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - let matches = delegate.matches.search.clone(); - - // Simulate a search being cancelled after the time limit, - // returning only a subset of the matches that would have been found. - drop(delegate.spawn_search(query.clone(), cx)); - delegate.set_search_matches( - delegate.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[1].clone(), matches[3].clone()], - cx, - ); - - // Simulate another cancellation. - drop(delegate.spawn_search(query.clone(), cx)); - delegate.set_search_matches( - delegate.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], - cx, - ); - - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); - }); - } - - #[gpui::test] - async fn test_ignored_files(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/ancestor", - json!({ - ".gitignore": "ignored-root", - "ignored-root": { - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - }, - "tracked-root": { - ".gitignore": "height", - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - }, - }), - ) - .await; - - let project = Project::test( - app_state.fs.clone(), - [ - "/ancestor/tracked-root".as_ref(), - "/ancestor/ignored-root".as_ref(), - ], - cx, - ) - .await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - let finder = cx - .add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), - cx, - ), - cx, - ) - }) - .root(cx); - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("hi"), cx) - }) - .await; - finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); - } - - #[gpui::test] - async fn test_single_file_worktrees(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) - .await; - - let project = Project::test( - app_state.fs.clone(), - ["/root/the-parent-dir/the-file".as_ref()], - cx, - ) - .await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - let finder = cx - .add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), - cx, - ), - cx, - ) - }) - .root(cx); - - // Even though there is only one worktree, that worktree's filename - // is included in the matching, because the worktree is a single file. - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("thf"), cx) - }) - .await; - cx.read(|cx| { - let finder = finder.read(cx); - let delegate = finder.delegate(); - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - let matches = delegate.matches.search.clone(); - assert_eq!(matches.len(), 1); - - let (file_name, file_name_positions, full_path, full_path_positions) = - delegate.labels_for_path_match(&matches[0]); - assert_eq!(file_name, "the-file"); - assert_eq!(file_name_positions, &[0, 1, 4]); - assert_eq!(full_path, "the-file"); - assert_eq!(full_path_positions, &[0, 1, 4]); - }); - - // Since the worktree root is a file, searching for its name followed by a slash does - // not match anything. - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("thf/"), cx) - }) - .await; - finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); - } - - #[gpui::test] - async fn test_path_distance_ordering(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "dir1": { "a.txt": "" }, - "dir2": { - "a.txt": "", - "b.txt": "" - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - let worktree_id = cx.read(|cx| { - let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - WorktreeId::from_usize(worktrees[0].id()) - }); - - // When workspace has an active item, sort items which are closer to that item - // first when they have the same name. In this case, b.txt is closer to dir2's a.txt - // so that one should be sorted earlier - let b_path = Some(dummy_found_path(ProjectPath { - worktree_id, - path: Arc::from(Path::new("/root/dir2/b.txt")), - })); - let finder = cx - .add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - b_path, - Vec::new(), - cx, - ), - cx, - ) - }) - .root(cx); - - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("a.txt"), cx) - }) - .await; - - finder.read_with(cx, |f, _| { - let delegate = f.delegate(); - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - let matches = delegate.matches.search.clone(); - assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); - assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); - }); - } - - #[gpui::test] - async fn test_search_worktree_without_files(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "dir1": {}, - "dir2": { - "dir3": {} - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - let finder = cx - .add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), - cx, - ), - cx, - ) - }) - .root(cx); - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("dir"), cx) - }) - .await; - cx.read(|cx| { - let finder = finder.read(cx); - assert_eq!(finder.delegate().matches.len(), 0); - }); - } - - #[gpui::test] - async fn test_query_history( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - let worktree_id = cx.read(|cx| { - let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - WorktreeId::from_usize(worktrees[0].id()) - }); - - // Open and close panels, getting their history items afterwards. - // Ensure history items get populated with opened items, and items are kept in a certain order. - // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. - // - // TODO: without closing, the opened items do not propagate their history changes for some reason - // it does work in real app though, only tests do not propagate. - - let initial_history = open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert!( - initial_history.is_empty(), - "Should have no history before opening any files" - ); - - let history_after_first = open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - history_after_first, - vec![FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/test/first.rs")) - )], - "Should show 1st opened item in the history when opening the 2nd item" - ); - - let history_after_second = open_close_queried_buffer( - "thi", - 1, - "third.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - history_after_second, - vec![ - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/second.rs")), - }, - Some(PathBuf::from("/src/test/second.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/test/first.rs")) - ), - ], - "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ -2nd item should be the first in the history, as the last opened." - ); - - let history_after_third = open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - history_after_third, - vec![ - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/third.rs")), - }, - Some(PathBuf::from("/src/test/third.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/second.rs")), - }, - Some(PathBuf::from("/src/test/second.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/test/first.rs")) - ), - ], - "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ -3rd item should be the first in the history, as the last opened." - ); - - let history_after_second_again = open_close_queried_buffer( - "thi", - 1, - "third.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - history_after_second_again, - vec![ - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/second.rs")), - }, - Some(PathBuf::from("/src/test/second.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/third.rs")), - }, - Some(PathBuf::from("/src/test/third.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/test/first.rs")) - ), - ], - "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ -2nd item, as the last opened, 3rd item should go next as it was opened right before." - ); - } - - #[gpui::test] - async fn test_external_files_history( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - } - }), - ) - .await; - - app_state - .fs - .as_fake() - .insert_tree( - "/external-src", - json!({ - "test": { - "third.rs": "// Third Rust file", - "fourth.rs": "// Fourth Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - cx.update(|cx| { - project.update(cx, |project, cx| { - project.find_or_create_local_worktree("/external-src", false, cx) - }) - }) - .detach(); - deterministic.run_until_parked(); - - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - let worktree_id = cx.read(|cx| { - let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1,); - - WorktreeId::from_usize(worktrees[0].id()) - }); - workspace - .update(cx, |workspace, cx| { - workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) - }) - .detach(); - deterministic.run_until_parked(); - let external_worktree_id = cx.read(|cx| { - let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - assert_eq!( - worktrees.len(), - 2, - "External file should get opened in a new worktree" - ); - - WorktreeId::from_usize( - worktrees - .into_iter() - .find(|worktree| worktree.id() != worktree_id.to_usize()) - .expect("New worktree should have a different id") - .id(), - ) - }); - close_active_item(&workspace, &deterministic, cx).await; - - let initial_history_items = open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - initial_history_items, - vec![FoundPath::new( - ProjectPath { - worktree_id: external_worktree_id, - path: Arc::from(Path::new("")), - }, - Some(PathBuf::from("/external-src/test/third.rs")) - )], - "Should show external file with its full path in the history after it was open" - ); - - let updated_history_items = open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - updated_history_items, - vec![ - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/second.rs")), - }, - Some(PathBuf::from("/src/test/second.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id: external_worktree_id, - path: Arc::from(Path::new("")), - }, - Some(PathBuf::from("/external-src/test/third.rs")) - ), - ], - "Should keep external file with history updates", - ); - } - - #[gpui::test] - async fn test_toggle_panel_new_selections( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - - // generate some history to select from - open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "thi", - 1, - "third.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - let current_history = open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - - for expected_selected_index in 0..current_history.len() { - cx.dispatch_action(window.into(), Toggle); - let selected_index = cx.read(|cx| { - workspace - .read(cx) - .modal::() - .unwrap() - .read(cx) - .delegate() - .selected_index() - }); - assert_eq!( - selected_index, expected_selected_index, - "Should select the next item in the history" - ); - } - - cx.dispatch_action(window.into(), Toggle); - let selected_index = cx.read(|cx| { - workspace - .read(cx) - .modal::() - .unwrap() - .read(cx) - .delegate() - .selected_index() - }); - assert_eq!( - selected_index, 0, - "Should wrap around the history and start all over" - ); - } - - #[gpui::test] - async fn test_search_preserves_history_items( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - "fourth.rs": "// Fourth Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - let worktree_id = cx.read(|cx| { - let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1,); - - WorktreeId::from_usize(worktrees[0].id()) - }); - - // generate some history to select from - open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "thi", - 1, - "third.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - - cx.dispatch_action(window.into(), Toggle); - let first_query = "f"; - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder - .delegate_mut() - .update_matches(first_query.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let delegate = finder.delegate(); - assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); - let history_match = delegate.matches.history.first().unwrap(); - assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); - assert_eq!(history_match.0, FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/test/first.rs")) - )); - assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); - assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); - }); - - let second_query = "fsdasdsa"; - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder - .delegate_mut() - .update_matches(second_query.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let delegate = finder.delegate(); - assert!( - delegate.matches.history.is_empty(), - "No history entries should match {second_query}" - ); - assert!( - delegate.matches.search.is_empty(), - "No search entries should match {second_query}" - ); - }); - - let first_query_again = first_query; - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder - .delegate_mut() - .update_matches(first_query_again.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let delegate = finder.delegate(); - assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); - let history_match = delegate.matches.history.first().unwrap(); - assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); - assert_eq!(history_match.0, FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/test/first.rs")) - )); - assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); - assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); - }); - } - - #[gpui::test] - async fn test_history_items_vs_very_good_external_match( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "collab_ui": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - "collab_ui.rs": "// Fourth Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - // generate some history to select from - open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "thi", - 1, - "third.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - - cx.dispatch_action(window.into(), Toggle); - let query = "collab_ui"; - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder.delegate_mut().update_matches(query.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let delegate = finder.delegate(); - assert!( - delegate.matches.history.is_empty(), - "History items should not math query {query}, they should be matched by name only" - ); - - let search_entries = delegate - .matches - .search - .iter() - .map(|path_match| path_match.path.to_path_buf()) - .collect::>(); - assert_eq!( - search_entries, - vec![ - PathBuf::from("collab_ui/collab_ui.rs"), - PathBuf::from("collab_ui/third.rs"), - PathBuf::from("collab_ui/first.rs"), - PathBuf::from("collab_ui/second.rs"), - ], - "Despite all search results having the same directory name, the most matching one should be on top" - ); - }); - } - - #[gpui::test] - async fn test_nonexistent_history_items_not_shown( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "nonexistent.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - // generate some history to select from - open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "non", - 1, - "nonexistent.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "thi", - 1, - "third.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - - cx.dispatch_action(window.into(), Toggle); - let query = "rs"; - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder.delegate_mut().update_matches(query.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let delegate = finder.delegate(); - let history_entries = delegate - .matches - .history - .iter() - .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) - .collect::>(); - assert_eq!( - history_entries, - vec![ - PathBuf::from("test/first.rs"), - PathBuf::from("test/third.rs"), - ], - "Should have all opened files in the history, except the ones that do not exist on disk" - ); - }); - } - - async fn open_close_queried_buffer( - input: &str, - expected_matches: usize, - expected_editor_title: &str, - window: gpui::AnyWindowHandle, - workspace: &ViewHandle, - deterministic: &gpui::executor::Deterministic, - cx: &mut gpui::TestAppContext, - ) -> Vec { - cx.dispatch_action(window, Toggle); - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder.delegate_mut().update_matches(input.to_string(), cx) - }) - .await; - let history_items = finder.read_with(cx, |finder, _| { - assert_eq!( - finder.delegate().matches.len(), - expected_matches, - "Unexpected number of matches found for query {input}" - ); - finder.delegate().history_items.clone() - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window, SelectNext); - cx.dispatch_action(window, Confirm); - deterministic.run_until_parked(); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - cx.read(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - let active_editor_title = active_item - .as_any() - .downcast_ref::() - .unwrap() - .read(cx) - .title(cx); - assert_eq!( - expected_editor_title, active_editor_title, - "Unexpected editor title for query {input}" - ); - }); - - close_active_item(workspace, deterministic, cx).await; - - history_items - } - - async fn close_active_item( - workspace: &ViewHandle, - deterministic: &gpui::executor::Deterministic, - cx: &mut TestAppContext, - ) { - let mut original_items = HashMap::new(); - cx.read(|cx| { - for pane in workspace.read(cx).panes() { - let pane_id = pane.id(); - let pane = pane.read(cx); - let insertion_result = original_items.insert(pane_id, pane.items().count()); - assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); - } - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - active_pane - .update(cx, |pane, cx| { - pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) - .unwrap() - }) - .await - .unwrap(); - deterministic.run_until_parked(); - cx.read(|cx| { - for pane in workspace.read(cx).panes() { - let pane_id = pane.id(); - let pane = pane.read(cx); - match original_items.remove(&pane_id) { - Some(original_items) => { - assert_eq!( - pane.items().count(), - original_items.saturating_sub(1), - "Pane id {pane_id} should have item closed" - ); - } - None => panic!("Pane id {pane_id} not found in original items"), - } - } - }); - assert!( - original_items.len() <= 1, - "At most one panel should got closed" - ); - } - - fn init_test(cx: &mut TestAppContext) -> Arc { - cx.foreground().forbid_parking(); - cx.update(|cx| { - let state = AppState::test(cx); - theme::init((), cx); - language::init(cx); - super::init(cx); - editor::init(cx); - workspace::init_settings(cx); - Project::init_settings(cx); - state - }) - } - - fn test_path_like(test_str: &str) -> PathLikeWithPosition { - PathLikeWithPosition::parse_str(test_str, |path_like_str| { - Ok::<_, std::convert::Infallible>(FileSearchQuery { - raw_query: test_str.to_owned(), - file_query_end: if path_like_str == test_str { - None - } else { - Some(path_like_str.len()) - }, - }) - }) - .unwrap() - } - - fn dummy_found_path(project_path: ProjectPath) -> FoundPath { - FoundPath { - project: project_path, - absolute: None, - } - } -} +// #[cfg(test)] +// mod tests { +// use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; + +// use super::*; +// use editor::Editor; +// use gpui::{TestAppContext, ViewHandle}; +// use menu::{Confirm, SelectNext}; +// use serde_json::json; +// use workspace::{AppState, Workspace}; + +// #[ctor::ctor] +// fn init_logger() { +// if std::env::var("RUST_LOG").is_ok() { +// env_logger::init(); +// } +// } + +// #[gpui::test] +// async fn test_matching_paths(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "banana": "", +// "bandana": "", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// cx.dispatch_action(window.into(), Toggle); + +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate_mut().update_matches("bna".to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// assert_eq!(finder.delegate().matches.len(), 2); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(window.into(), SelectNext); +// cx.dispatch_action(window.into(), Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// cx.read(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// assert_eq!( +// active_item +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx), +// "bandana" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let first_file_name = "first.rs"; +// let first_file_contents = "// First Rust file"; +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// first_file_name: first_file_contents, +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// cx.dispatch_action(window.into(), Toggle); +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + +// let file_query = &first_file_name[..3]; +// let file_row = 1; +// let file_column = 3; +// assert!(file_column <= first_file_contents.len()); +// let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate_mut() +// .update_matches(query_inside_file.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let finder = finder.delegate(); +// assert_eq!(finder.matches.len(), 1); +// let latest_search_query = finder +// .latest_search_query +// .as_ref() +// .expect("Finder should have a query after the update_matches call"); +// assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); +// assert_eq!( +// latest_search_query.path_like.file_query_end, +// Some(file_query.len()) +// ); +// assert_eq!(latest_search_query.row, Some(file_row)); +// assert_eq!(latest_search_query.column, Some(file_column as u32)); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(window.into(), SelectNext); +// cx.dispatch_action(window.into(), Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// let editor = cx.update(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// active_item.downcast::().unwrap() +// }); +// cx.foreground().advance_clock(Duration::from_secs(2)); +// cx.foreground().start_waiting(); +// cx.foreground().finish_waiting(); +// editor.update(cx, |editor, cx| { +// let all_selections = editor.selections.all_adjusted(cx); +// assert_eq!( +// all_selections.len(), +// 1, +// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" +// ); +// let caret_selection = all_selections.into_iter().next().unwrap(); +// assert_eq!(caret_selection.start, caret_selection.end, +// "Caret selection should have its start and end at the same position"); +// assert_eq!(file_row, caret_selection.start.row + 1, +// "Query inside file should get caret with the same focus row"); +// assert_eq!(file_column, caret_selection.start.column as usize + 1, +// "Query inside file should get caret with the same focus column"); +// }); +// } + +// #[gpui::test] +// async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let first_file_name = "first.rs"; +// let first_file_contents = "// First Rust file"; +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// first_file_name: first_file_contents, +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// cx.dispatch_action(window.into(), Toggle); +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + +// let file_query = &first_file_name[..3]; +// let file_row = 200; +// let file_column = 300; +// assert!(file_column > first_file_contents.len()); +// let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate_mut() +// .update_matches(query_outside_file.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let finder = finder.delegate(); +// assert_eq!(finder.matches.len(), 1); +// let latest_search_query = finder +// .latest_search_query +// .as_ref() +// .expect("Finder should have a query after the update_matches call"); +// assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); +// assert_eq!( +// latest_search_query.path_like.file_query_end, +// Some(file_query.len()) +// ); +// assert_eq!(latest_search_query.row, Some(file_row)); +// assert_eq!(latest_search_query.column, Some(file_column as u32)); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(window.into(), SelectNext); +// cx.dispatch_action(window.into(), Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// let editor = cx.update(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// active_item.downcast::().unwrap() +// }); +// cx.foreground().advance_clock(Duration::from_secs(2)); +// cx.foreground().start_waiting(); +// cx.foreground().finish_waiting(); +// editor.update(cx, |editor, cx| { +// let all_selections = editor.selections.all_adjusted(cx); +// assert_eq!( +// all_selections.len(), +// 1, +// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" +// ); +// let caret_selection = all_selections.into_iter().next().unwrap(); +// assert_eq!(caret_selection.start, caret_selection.end, +// "Caret selection should have its start and end at the same position"); +// assert_eq!(0, caret_selection.start.row, +// "Excessive rows (as in query outside file borders) should get trimmed to last file row"); +// assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, +// "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); +// }); +// } + +// #[gpui::test] +// async fn test_matching_cancellation(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/dir", +// json!({ +// "hello": "", +// "goodbye": "", +// "halogen-light": "", +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// None, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); + +// let query = test_path_like("hi"); +// finder +// .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) +// .await; +// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5)); + +// finder.update(cx, |finder, cx| { +// let delegate = finder.delegate_mut(); +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); + +// // Simulate a search being cancelled after the time limit, +// // returning only a subset of the matches that would have been found. +// drop(delegate.spawn_search(query.clone(), cx)); +// delegate.set_search_matches( +// delegate.latest_search_id, +// true, // did-cancel +// query.clone(), +// vec![matches[1].clone(), matches[3].clone()], +// cx, +// ); + +// // Simulate another cancellation. +// drop(delegate.spawn_search(query.clone(), cx)); +// delegate.set_search_matches( +// delegate.latest_search_id, +// true, // did-cancel +// query.clone(), +// vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], +// cx, +// ); + +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); +// }); +// } + +// #[gpui::test] +// async fn test_ignored_files(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/ancestor", +// json!({ +// ".gitignore": "ignored-root", +// "ignored-root": { +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }, +// "tracked-root": { +// ".gitignore": "height", +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }, +// }), +// ) +// .await; + +// let project = Project::test( +// app_state.fs.clone(), +// [ +// "/ancestor/tracked-root".as_ref(), +// "/ancestor/ignored-root".as_ref(), +// ], +// cx, +// ) +// .await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// None, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); +// finder +// .update(cx, |f, cx| { +// f.delegate_mut().spawn_search(test_path_like("hi"), cx) +// }) +// .await; +// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); +// } + +// #[gpui::test] +// async fn test_single_file_worktrees(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) +// .await; + +// let project = Project::test( +// app_state.fs.clone(), +// ["/root/the-parent-dir/the-file".as_ref()], +// cx, +// ) +// .await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// None, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); + +// // Even though there is only one worktree, that worktree's filename +// // is included in the matching, because the worktree is a single file. +// finder +// .update(cx, |f, cx| { +// f.delegate_mut().spawn_search(test_path_like("thf"), cx) +// }) +// .await; +// cx.read(|cx| { +// let finder = finder.read(cx); +// let delegate = finder.delegate(); +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); +// assert_eq!(matches.len(), 1); + +// let (file_name, file_name_positions, full_path, full_path_positions) = +// delegate.labels_for_path_match(&matches[0]); +// assert_eq!(file_name, "the-file"); +// assert_eq!(file_name_positions, &[0, 1, 4]); +// assert_eq!(full_path, "the-file"); +// assert_eq!(full_path_positions, &[0, 1, 4]); +// }); + +// // Since the worktree root is a file, searching for its name followed by a slash does +// // not match anything. +// finder +// .update(cx, |f, cx| { +// f.delegate_mut().spawn_search(test_path_like("thf/"), cx) +// }) +// .await; +// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); +// } + +// #[gpui::test] +// async fn test_path_distance_ordering(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "dir1": { "a.txt": "" }, +// "dir2": { +// "a.txt": "", +// "b.txt": "" +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// WorktreeId::from_usize(worktrees[0].id()) +// }); + +// // When workspace has an active item, sort items which are closer to that item +// // first when they have the same name. In this case, b.txt is closer to dir2's a.txt +// // so that one should be sorted earlier +// let b_path = Some(dummy_found_path(ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("/root/dir2/b.txt")), +// })); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// b_path, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); + +// finder +// .update(cx, |f, cx| { +// f.delegate_mut().spawn_search(test_path_like("a.txt"), cx) +// }) +// .await; + +// finder.read_with(cx, |f, _| { +// let delegate = f.delegate(); +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); +// assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); +// assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); +// }); +// } + +// #[gpui::test] +// async fn test_search_worktree_without_files(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "dir1": {}, +// "dir2": { +// "dir3": {} +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// None, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); +// finder +// .update(cx, |f, cx| { +// f.delegate_mut().spawn_search(test_path_like("dir"), cx) +// }) +// .await; +// cx.read(|cx| { +// let finder = finder.read(cx); +// assert_eq!(finder.delegate().matches.len(), 0); +// }); +// } + +// #[gpui::test] +// async fn test_query_history( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// WorktreeId::from_usize(worktrees[0].id()) +// }); + +// // Open and close panels, getting their history items afterwards. +// // Ensure history items get populated with opened items, and items are kept in a certain order. +// // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. +// // +// // TODO: without closing, the opened items do not propagate their history changes for some reason +// // it does work in real app though, only tests do not propagate. + +// let initial_history = open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert!( +// initial_history.is_empty(), +// "Should have no history before opening any files" +// ); + +// let history_after_first = open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// history_after_first, +// vec![FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )], +// "Should show 1st opened item in the history when opening the 2nd item" +// ); + +// let history_after_second = open_close_queried_buffer( +// "thi", +// 1, +// "third.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// history_after_second, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// ), +// ], +// "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ +// 2nd item should be the first in the history, as the last opened." +// ); + +// let history_after_third = open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// history_after_third, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/third.rs")), +// }, +// Some(PathBuf::from("/src/test/third.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// ), +// ], +// "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ +// 3rd item should be the first in the history, as the last opened." +// ); + +// let history_after_second_again = open_close_queried_buffer( +// "thi", +// 1, +// "third.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// history_after_second_again, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/third.rs")), +// }, +// Some(PathBuf::from("/src/test/third.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// ), +// ], +// "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ +// 2nd item, as the last opened, 3rd item should go next as it was opened right before." +// ); +// } + +// #[gpui::test] +// async fn test_external_files_history( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/external-src", +// json!({ +// "test": { +// "third.rs": "// Third Rust file", +// "fourth.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// cx.update(|cx| { +// project.update(cx, |project, cx| { +// project.find_or_create_local_worktree("/external-src", false, cx) +// }) +// }) +// .detach(); +// deterministic.run_until_parked(); + +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1,); + +// WorktreeId::from_usize(worktrees[0].id()) +// }); +// workspace +// .update(cx, |workspace, cx| { +// workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) +// }) +// .detach(); +// deterministic.run_until_parked(); +// let external_worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!( +// worktrees.len(), +// 2, +// "External file should get opened in a new worktree" +// ); + +// WorktreeId::from_usize( +// worktrees +// .into_iter() +// .find(|worktree| worktree.id() != worktree_id.to_usize()) +// .expect("New worktree should have a different id") +// .id(), +// ) +// }); +// close_active_item(&workspace, &deterministic, cx).await; + +// let initial_history_items = open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// initial_history_items, +// vec![FoundPath::new( +// ProjectPath { +// worktree_id: external_worktree_id, +// path: Arc::from(Path::new("")), +// }, +// Some(PathBuf::from("/external-src/test/third.rs")) +// )], +// "Should show external file with its full path in the history after it was open" +// ); + +// let updated_history_items = open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// updated_history_items, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id: external_worktree_id, +// path: Arc::from(Path::new("")), +// }, +// Some(PathBuf::from("/external-src/test/third.rs")) +// ), +// ], +// "Should keep external file with history updates", +// ); +// } + +// #[gpui::test] +// async fn test_toggle_panel_new_selections( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// // generate some history to select from +// open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "thi", +// 1, +// "third.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// let current_history = open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; + +// for expected_selected_index in 0..current_history.len() { +// cx.dispatch_action(window.into(), Toggle); +// let selected_index = cx.read(|cx| { +// workspace +// .read(cx) +// .modal::() +// .unwrap() +// .read(cx) +// .delegate() +// .selected_index() +// }); +// assert_eq!( +// selected_index, expected_selected_index, +// "Should select the next item in the history" +// ); +// } + +// cx.dispatch_action(window.into(), Toggle); +// let selected_index = cx.read(|cx| { +// workspace +// .read(cx) +// .modal::() +// .unwrap() +// .read(cx) +// .delegate() +// .selected_index() +// }); +// assert_eq!( +// selected_index, 0, +// "Should wrap around the history and start all over" +// ); +// } + +// #[gpui::test] +// async fn test_search_preserves_history_items( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// "fourth.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1,); + +// WorktreeId::from_usize(worktrees[0].id()) +// }); + +// // generate some history to select from +// open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "thi", +// 1, +// "third.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; + +// cx.dispatch_action(window.into(), Toggle); +// let first_query = "f"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate_mut() +// .update_matches(first_query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = finder.delegate(); +// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); +// let history_match = delegate.matches.history.first().unwrap(); +// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); +// assert_eq!(history_match.0, FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )); +// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); +// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); +// }); + +// let second_query = "fsdasdsa"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate_mut() +// .update_matches(second_query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = finder.delegate(); +// assert!( +// delegate.matches.history.is_empty(), +// "No history entries should match {second_query}" +// ); +// assert!( +// delegate.matches.search.is_empty(), +// "No search entries should match {second_query}" +// ); +// }); + +// let first_query_again = first_query; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate_mut() +// .update_matches(first_query_again.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = finder.delegate(); +// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); +// let history_match = delegate.matches.history.first().unwrap(); +// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); +// assert_eq!(history_match.0, FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )); +// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); +// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); +// }); +// } + +// #[gpui::test] +// async fn test_history_items_vs_very_good_external_match( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "collab_ui": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// "collab_ui.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// // generate some history to select from +// open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "thi", +// 1, +// "third.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; + +// cx.dispatch_action(window.into(), Toggle); +// let query = "collab_ui"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate_mut().update_matches(query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = finder.delegate(); +// assert!( +// delegate.matches.history.is_empty(), +// "History items should not math query {query}, they should be matched by name only" +// ); + +// let search_entries = delegate +// .matches +// .search +// .iter() +// .map(|path_match| path_match.path.to_path_buf()) +// .collect::>(); +// assert_eq!( +// search_entries, +// vec![ +// PathBuf::from("collab_ui/collab_ui.rs"), +// PathBuf::from("collab_ui/third.rs"), +// PathBuf::from("collab_ui/first.rs"), +// PathBuf::from("collab_ui/second.rs"), +// ], +// "Despite all search results having the same directory name, the most matching one should be on top" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_nonexistent_history_items_not_shown( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "nonexistent.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// // generate some history to select from +// open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "non", +// 1, +// "nonexistent.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "thi", +// 1, +// "third.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; + +// cx.dispatch_action(window.into(), Toggle); +// let query = "rs"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate_mut().update_matches(query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = finder.delegate(); +// let history_entries = delegate +// .matches +// .history +// .iter() +// .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) +// .collect::>(); +// assert_eq!( +// history_entries, +// vec![ +// PathBuf::from("test/first.rs"), +// PathBuf::from("test/third.rs"), +// ], +// "Should have all opened files in the history, except the ones that do not exist on disk" +// ); +// }); +// } + +// async fn open_close_queried_buffer( +// input: &str, +// expected_matches: usize, +// expected_editor_title: &str, +// window: gpui::AnyWindowHandle, +// workspace: &ViewHandle, +// deterministic: &gpui::executor::Deterministic, +// cx: &mut gpui::TestAppContext, +// ) -> Vec { +// cx.dispatch_action(window, Toggle); +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate_mut().update_matches(input.to_string(), cx) +// }) +// .await; +// let history_items = finder.read_with(cx, |finder, _| { +// assert_eq!( +// finder.delegate().matches.len(), +// expected_matches, +// "Unexpected number of matches found for query {input}" +// ); +// finder.delegate().history_items.clone() +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(window, SelectNext); +// cx.dispatch_action(window, Confirm); +// deterministic.run_until_parked(); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// cx.read(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// let active_editor_title = active_item +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx); +// assert_eq!( +// expected_editor_title, active_editor_title, +// "Unexpected editor title for query {input}" +// ); +// }); + +// close_active_item(workspace, deterministic, cx).await; + +// history_items +// } + +// async fn close_active_item( +// workspace: &ViewHandle, +// deterministic: &gpui::executor::Deterministic, +// cx: &mut TestAppContext, +// ) { +// let mut original_items = HashMap::new(); +// cx.read(|cx| { +// for pane in workspace.read(cx).panes() { +// let pane_id = pane.id(); +// let pane = pane.read(cx); +// let insertion_result = original_items.insert(pane_id, pane.items().count()); +// assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); +// } +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// active_pane +// .update(cx, |pane, cx| { +// pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) +// .unwrap() +// }) +// .await +// .unwrap(); +// deterministic.run_until_parked(); +// cx.read(|cx| { +// for pane in workspace.read(cx).panes() { +// let pane_id = pane.id(); +// let pane = pane.read(cx); +// match original_items.remove(&pane_id) { +// Some(original_items) => { +// assert_eq!( +// pane.items().count(), +// original_items.saturating_sub(1), +// "Pane id {pane_id} should have item closed" +// ); +// } +// None => panic!("Pane id {pane_id} not found in original items"), +// } +// } +// }); +// assert!( +// original_items.len() <= 1, +// "At most one panel should got closed" +// ); +// } + +// fn init_test(cx: &mut TestAppContext) -> Arc { +// cx.foreground_executor().forbid_parking(); +// cx.update(|cx| { +// let state = AppState::test(cx); +// theme::init(cx); +// language::init(cx); +// super::init(cx); +// editor::init(cx); +// workspace::init_settings(cx); +// Project::init_settings(cx); +// state +// }) +// } + +// fn test_path_like(test_str: &str) -> PathLikeWithPosition { +// PathLikeWithPosition::parse_str(test_str, |path_like_str| { +// Ok::<_, std::convert::Infallible>(FileSearchQuery { +// raw_query: test_str.to_owned(), +// file_query_end: if path_like_str == test_str { +// None +// } else { +// Some(path_like_str.len()) +// }, +// }) +// }) +// .unwrap() +// } + +// fn dummy_found_path(project_path: ProjectPath) -> FoundPath { +// FoundPath { +// project: project_path, +// absolute: None, +// } +// } +// } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 97f4262623..f4b8d15d75 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -146,6 +146,11 @@ impl Picker { } } + pub fn refresh(&mut self, cx: &mut ViewContext) { + let query = self.editor.read(cx).text(cx); + self.update_matches(query, cx); + } + pub fn update_matches(&mut self, query: String, cx: &mut ViewContext) { let update = self.delegate.update_matches(query, cx); self.matches_updated(cx); diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 4ee136f47a..b2b78a2391 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -1961,50 +1961,50 @@ impl Workspace { }) } - // pub fn open_abs_path( - // &mut self, - // abs_path: PathBuf, - // visible: bool, - // cx: &mut ViewContext, - // ) -> Task>> { - // cx.spawn(|workspace, mut cx| async move { - // let open_paths_task_result = workspace - // .update(&mut cx, |workspace, cx| { - // workspace.open_paths(vec![abs_path.clone()], visible, cx) - // }) - // .with_context(|| format!("open abs path {abs_path:?} task spawn"))? - // .await; - // anyhow::ensure!( - // open_paths_task_result.len() == 1, - // "open abs path {abs_path:?} task returned incorrect number of results" - // ); - // match open_paths_task_result - // .into_iter() - // .next() - // .expect("ensured single task result") - // { - // Some(open_result) => { - // open_result.with_context(|| format!("open abs path {abs_path:?} task join")) - // } - // None => anyhow::bail!("open abs path {abs_path:?} task returned None"), - // } - // }) - // } + pub fn open_abs_path( + &mut self, + abs_path: PathBuf, + visible: bool, + cx: &mut ViewContext, + ) -> Task>> { + cx.spawn(|workspace, mut cx| async move { + let open_paths_task_result = workspace + .update(&mut cx, |workspace, cx| { + workspace.open_paths(vec![abs_path.clone()], visible, cx) + }) + .with_context(|| format!("open abs path {abs_path:?} task spawn"))? + .await; + anyhow::ensure!( + open_paths_task_result.len() == 1, + "open abs path {abs_path:?} task returned incorrect number of results" + ); + match open_paths_task_result + .into_iter() + .next() + .expect("ensured single task result") + { + Some(open_result) => { + open_result.with_context(|| format!("open abs path {abs_path:?} task join")) + } + None => anyhow::bail!("open abs path {abs_path:?} task returned None"), + } + }) + } - // pub fn split_abs_path( - // &mut self, - // abs_path: PathBuf, - // visible: bool, - // cx: &mut ViewContext, - // ) -> Task>> { - // let project_path_task = - // Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx); - // cx.spawn(|this, mut cx| async move { - // let (_, path) = project_path_task.await?; - // this.update(&mut cx, |this, cx| this.split_path(path, cx))? - // .await - // }) - // } + pub fn split_abs_path( + &mut self, + abs_path: PathBuf, + visible: bool, + cx: &mut ViewContext, + ) -> Task>> { + let project_path_task = + Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx); + cx.spawn(|this, mut cx| async move { + let (_, path) = project_path_task.await?; + this.update(&mut cx, |this, cx| this.split_path(path, cx))? + .await + }) + } pub fn open_path( &mut self, @@ -2031,37 +2031,37 @@ impl Workspace { }) } - // pub fn split_path( - // &mut self, - // path: impl Into, - // cx: &mut ViewContext, - // ) -> Task, anyhow::Error>> { - // let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { - // self.panes - // .first() - // .expect("There must be an active pane") - // .downgrade() - // }); + pub fn split_path( + &mut self, + path: impl Into, + cx: &mut ViewContext, + ) -> Task, anyhow::Error>> { + let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { + self.panes + .first() + .expect("There must be an active pane") + .downgrade() + }); - // if let Member::Pane(center_pane) = &self.center.root { - // if center_pane.read(cx).items_len() == 0 { - // return self.open_path(path, Some(pane), true, cx); - // } - // } + if let Member::Pane(center_pane) = &self.center.root { + if center_pane.read(cx).items_len() == 0 { + return self.open_path(path, Some(pane), true, cx); + } + } - // let task = self.load_path(path.into(), cx); - // cx.spawn(|this, mut cx| async move { - // let (project_entry_id, build_item) = task.await?; - // this.update(&mut cx, move |this, cx| -> Option<_> { - // let pane = pane.upgrade(cx)?; - // let new_pane = this.split_pane(pane, SplitDirection::Right, cx); - // new_pane.update(cx, |new_pane, cx| { - // Some(new_pane.open_item(project_entry_id, true, cx, build_item)) - // }) - // }) - // .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))? - // }) - // } + let task = self.load_path(path.into(), cx); + cx.spawn(|this, mut cx| async move { + let (project_entry_id, build_item) = task.await?; + this.update(&mut cx, move |this, cx| -> Option<_> { + let pane = pane.upgrade()?; + let new_pane = this.split_pane(pane, SplitDirection::Right, cx); + new_pane.update(cx, |new_pane, cx| { + Some(new_pane.open_item(project_entry_id, true, cx, build_item)) + }) + }) + .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))? + }) + } pub(crate) fn load_path( &mut self, From 22f024bd5f65f4005c2276a053b54d7f11382d48 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 14 Nov 2023 15:44:26 -0500 Subject: [PATCH 109/126] Use `IconElement` in project panel --- crates/project_panel2/src/project_panel.rs | 13 +++---------- crates/ui2/src/components/icon.rs | 14 +++++++++++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 1feead1a19..1f415e899e 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -8,7 +8,7 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ - actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, + actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, ClipboardItem, Component, Div, EventEmitter, FocusHandle, FocusableKeyDispatch, Model, MouseButton, ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, @@ -31,7 +31,7 @@ use std::{ sync::Arc, }; use theme::ActiveTheme as _; -use ui::{h_stack, v_stack, Label}; +use ui::{h_stack, v_stack, IconElement, Label}; use unicase::UniCase; use util::{maybe, TryFutureExt}; use workspace::{ @@ -1353,14 +1353,7 @@ impl ProjectPanel { h_stack() .child(if let Some(icon) = &details.icon { - div().child( - // todo!() Marshall: Can we use our `IconElement` component here? - svg() - .size(rems(0.9375)) - .flex_none() - .path(icon.to_string()) - .text_color(cx.theme().colors().icon), - ) + div().child(IconElement::from_path(icon.to_string())) } else { div() }) diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 5b60421205..a0ef496d18 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -129,7 +129,7 @@ impl Icon { #[derive(Component)] pub struct IconElement { - icon: Icon, + path: SharedString, color: TextColor, size: IconSize, } @@ -137,7 +137,15 @@ pub struct IconElement { impl IconElement { pub fn new(icon: Icon) -> Self { Self { - icon, + path: icon.path().into(), + color: TextColor::default(), + size: IconSize::default(), + } + } + + pub fn from_path(path: impl Into) -> Self { + Self { + path: path.into(), color: TextColor::default(), size: IconSize::default(), } @@ -162,7 +170,7 @@ impl IconElement { svg() .size(svg_size) .flex_none() - .path(self.icon.path()) + .path(self.path) .text_color(self.color.color(cx)) } } From 123faed5b014357917a6ac6ebb67c2c67bb5e1a9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Nov 2023 12:41:20 -0800 Subject: [PATCH 110/126] Re-enable all project panel tests Some are still failing. --- crates/editor2/src/editor.rs | 4 + crates/gpui2/src/app/test_context.rs | 15 + crates/project_panel2/src/project_panel.rs | 2590 ++++++++++---------- 3 files changed, 1316 insertions(+), 1293 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ebe78d95b3..02365c3c23 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9168,6 +9168,10 @@ impl Editor { cx.focus(&self.focus_handle) } + pub fn is_focused(&self, cx: &WindowContext) -> bool { + self.focus_handle.is_focused(cx) + } + fn handle_focus_in(&mut self, cx: &mut ViewContext) { if self.focus_handle.is_focused(cx) { // todo!() diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 44c31bbd69..c223f20532 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -152,6 +152,21 @@ impl TestAppContext { (view, VisualTestContext::from_window(*window.deref(), self)) } + pub fn simulate_new_path_selection( + &self, + _select_path: impl FnOnce(&std::path::Path) -> Option, + ) { + // + } + + pub fn simulate_prompt_answer(&self, _button_ix: usize) { + // + } + + pub fn has_pending_prompt(&self) -> bool { + false + } + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 1feead1a19..e16ea364bd 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -1577,1296 +1577,1300 @@ impl ClipboardEntry { } } -// todo!() -// #[cfg(test)] -// mod tests { -// use super::*; -// use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle}; -// use pretty_assertions::assert_eq; -// use project::FakeFs; -// use serde_json::json; -// use settings::SettingsStore; -// use std::{ -// collections::HashSet, -// path::{Path, PathBuf}, -// sync::atomic::{self, AtomicUsize}, -// }; -// use workspace::{pane, AppState}; - -// #[gpui::test] -// async fn test_visible_list(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.executor().clone()); -// fs.insert_tree( -// "/root1", -// json!({ -// ".dockerignore": "", -// ".git": { -// "HEAD": "", -// }, -// "a": { -// "0": { "q": "", "r": "", "s": "" }, -// "1": { "t": "", "u": "" }, -// "2": { "v": "", "w": "", "x": "", "y": "" }, -// }, -// "b": { -// "3": { "Q": "" }, -// "4": { "R": "", "S": "", "T": "", "U": "" }, -// }, -// "C": { -// "5": {}, -// "6": { "V": "", "W": "" }, -// "7": { "X": "" }, -// "8": { "Y": {}, "Z": "" } -// } -// }), -// ) -// .await; -// fs.insert_tree( -// "/root2", -// json!({ -// "d": { -// "9": "" -// }, -// "e": {} -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..50, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// toggle_expand_dir(&panel, "root1/b", cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..50, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b <== selected", -// " > 3", -// " > 4", -// " > C", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// assert_eq!( -// visible_entries_as_strings(&panel, 6..9, cx), -// &[ -// // -// " > C", -// " .dockerignore", -// "v root2", -// ] -// ); -// } - -// #[gpui::test(iterations = 30)] -// async fn test_editing_files(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/root1", -// json!({ -// ".dockerignore": "", -// ".git": { -// "HEAD": "", -// }, -// "a": { -// "0": { "q": "", "r": "", "s": "" }, -// "1": { "t": "", "u": "" }, -// "2": { "v": "", "w": "", "x": "", "y": "" }, -// }, -// "b": { -// "3": { "Q": "" }, -// "4": { "R": "", "S": "", "T": "", "U": "" }, -// }, -// "C": { -// "5": {}, -// "6": { "V": "", "W": "" }, -// "7": { "X": "" }, -// "8": { "Y": {}, "Z": "" } -// } -// }), -// ) -// .await; -// fs.insert_tree( -// "/root2", -// json!({ -// "d": { -// "9": "" -// }, -// "e": {} -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// select_path(&panel, "root1", cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1 <== selected", -// " > .git", -// " > a", -// " > b", -// " > C", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// // Add a file with the root folder selected. The filename editor is placed -// // before the first file in the root folder. -// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); -// window.read_with(cx, |cx| { -// let panel = panel.read(cx); -// assert!(panel.filename_editor.is_focused(cx)); -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " [EDITOR: ''] <== selected", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// let confirm = panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); -// panel.confirm(&Confirm, cx).unwrap() -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " [PROCESSING: 'the-new-filename'] <== selected", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// confirm.await.unwrap(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " .dockerignore", -// " the-new-filename <== selected", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// select_path(&panel, "root1/b", cx); -// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " [EDITOR: ''] <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// panel -// .update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx)); -// panel.confirm(&Confirm, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " another-filename.txt <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// select_path(&panel, "root1/b/another-filename.txt", cx); -// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " [EDITOR: 'another-filename.txt'] <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// let confirm = panel.update(cx, |panel, cx| { -// panel.filename_editor.update(cx, |editor, cx| { -// let file_name_selections = editor.selections.all::(cx); -// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); -// let file_name_selection = &file_name_selections[0]; -// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); -// assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension"); - -// editor.set_text("a-different-filename.tar.gz", cx) -// }); -// panel.confirm(&Confirm, cx).unwrap() -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " [PROCESSING: 'a-different-filename.tar.gz'] <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// confirm.await.unwrap(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " a-different-filename.tar.gz <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " [EDITOR: 'a-different-filename.tar.gz'] <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// panel.update(cx, |panel, cx| { -// panel.filename_editor.update(cx, |editor, cx| { -// let file_name_selections = editor.selections.all::(cx); -// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); -// let file_name_selection = &file_name_selections[0]; -// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); -// assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot"); - -// }); -// panel.cancel(&Cancel, cx) -// }); - -// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > [EDITOR: ''] <== selected", -// " > 3", -// " > 4", -// " a-different-filename.tar.gz", -// " > C", -// " .dockerignore", -// ] -// ); - -// let confirm = panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("new-dir", cx)); -// panel.confirm(&Confirm, cx).unwrap() -// }); -// panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > [PROCESSING: 'new-dir']", -// " > 3 <== selected", -// " > 4", -// " a-different-filename.tar.gz", -// " > C", -// " .dockerignore", -// ] -// ); - -// confirm.await.unwrap(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3 <== selected", -// " > 4", -// " > new-dir", -// " a-different-filename.tar.gz", -// " > C", -// " .dockerignore", -// ] -// ); - -// panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > [EDITOR: '3'] <== selected", -// " > 4", -// " > new-dir", -// " a-different-filename.tar.gz", -// " > C", -// " .dockerignore", -// ] -// ); - -// // Dismiss the rename editor when it loses focus. -// workspace.update(cx, |_, cx| cx.focus_self()); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3 <== selected", -// " > 4", -// " > new-dir", -// " a-different-filename.tar.gz", -// " > C", -// " .dockerignore", -// ] -// ); -// } - -// #[gpui::test(iterations = 30)] -// async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/root1", -// json!({ -// ".dockerignore": "", -// ".git": { -// "HEAD": "", -// }, -// "a": { -// "0": { "q": "", "r": "", "s": "" }, -// "1": { "t": "", "u": "" }, -// "2": { "v": "", "w": "", "x": "", "y": "" }, -// }, -// "b": { -// "3": { "Q": "" }, -// "4": { "R": "", "S": "", "T": "", "U": "" }, -// }, -// "C": { -// "5": {}, -// "6": { "V": "", "W": "" }, -// "7": { "X": "" }, -// "8": { "Y": {}, "Z": "" } -// } -// }), -// ) -// .await; -// fs.insert_tree( -// "/root2", -// json!({ -// "d": { -// "9": "" -// }, -// "e": {} -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// select_path(&panel, "root1", cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1 <== selected", -// " > .git", -// " > a", -// " > b", -// " > C", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// // Add a file with the root folder selected. The filename editor is placed -// // before the first file in the root folder. -// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); -// window.read_with(cx, |cx| { -// let panel = panel.read(cx); -// assert!(panel.filename_editor.is_focused(cx)); -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " [EDITOR: ''] <== selected", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// let confirm = panel.update(cx, |panel, cx| { -// panel.filename_editor.update(cx, |editor, cx| { -// editor.set_text("/bdir1/dir2/the-new-filename", cx) -// }); -// panel.confirm(&Confirm, cx).unwrap() -// }); - -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// confirm.await.unwrap(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..13, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " v bdir1", -// " v dir2", -// " the-new-filename <== selected", -// " > C", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); -// } - -// #[gpui::test] -// async fn test_copy_paste(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/root1", -// json!({ -// "one.two.txt": "", -// "one.txt": "" -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// panel.update(cx, |panel, cx| { -// panel.select_next(&Default::default(), cx); -// panel.select_next(&Default::default(), cx); -// }); - -// assert_eq!( -// visible_entries_as_strings(&panel, 0..50, cx), -// &[ -// // -// "v root1", -// " one.two.txt <== selected", -// " one.txt", -// ] -// ); - -// // Regression test - file name is created correctly when -// // the copied file's name contains multiple dots. -// panel.update(cx, |panel, cx| { -// panel.copy(&Default::default(), cx); -// panel.paste(&Default::default(), cx); -// }); -// cx.foreground().run_until_parked(); - -// assert_eq!( -// visible_entries_as_strings(&panel, 0..50, cx), -// &[ -// // -// "v root1", -// " one.two copy.txt", -// " one.two.txt <== selected", -// " one.txt", -// ] -// ); - -// panel.update(cx, |panel, cx| { -// panel.paste(&Default::default(), cx); -// }); -// cx.foreground().run_until_parked(); - -// assert_eq!( -// visible_entries_as_strings(&panel, 0..50, cx), -// &[ -// // -// "v root1", -// " one.two copy 1.txt", -// " one.two copy.txt", -// " one.two.txt <== selected", -// " one.txt", -// ] -// ); -// } - -// #[gpui::test] -// async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { -// init_test_with_editor(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// toggle_expand_dir(&panel, "src/test", cx); -// select_path(&panel, "src/test/first.rs", cx); -// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " first.rs <== selected", -// " second.rs", -// " third.rs" -// ] -// ); -// ensure_single_file_is_opened(window, "test/first.rs", cx); - -// submit_deletion(window.into(), &panel, cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " second.rs", -// " third.rs" -// ], -// "Project panel should have no deleted file, no other file is selected in it" -// ); -// ensure_no_open_items_and_panes(window.into(), &workspace, cx); - -// select_path(&panel, "src/test/second.rs", cx); -// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " second.rs <== selected", -// " third.rs" -// ] -// ); -// ensure_single_file_is_opened(window, "test/second.rs", cx); - -// window.update(cx, |cx| { -// let active_items = workspace -// .read(cx) -// .panes() -// .iter() -// .filter_map(|pane| pane.read(cx).active_item()) -// .collect::>(); -// assert_eq!(active_items.len(), 1); -// let open_editor = active_items -// .into_iter() -// .next() -// .unwrap() -// .downcast::() -// .expect("Open item should be an editor"); -// open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); -// }); -// submit_deletion(window.into(), &panel, cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v src", " v test", " third.rs"], -// "Project panel should have no deleted file, with one last file remaining" -// ); -// ensure_no_open_items_and_panes(window.into(), &workspace, cx); -// } - -// #[gpui::test] -// async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { -// init_test_with_editor(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// select_path(&panel, "src/", cx); -// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v src <== selected", " > test"] -// ); -// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); -// window.read_with(cx, |cx| { -// let panel = panel.read(cx); -// assert!(panel.filename_editor.is_focused(cx)); -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v src", " > [EDITOR: ''] <== selected", " > test"] -// ); -// panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("test", cx)); -// assert!( -// panel.confirm(&Confirm, cx).is_none(), -// "Should not allow to confirm on conflicting new directory name" -// ) -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v src", " > test"], -// "File list should be unchanged after failed folder create confirmation" -// ); - -// select_path(&panel, "src/test/", cx); -// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v src", " > test <== selected"] -// ); -// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); -// window.read_with(cx, |cx| { -// let panel = panel.read(cx); -// assert!(panel.filename_editor.is_focused(cx)); -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " [EDITOR: ''] <== selected", -// " first.rs", -// " second.rs", -// " third.rs" -// ] -// ); -// panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("first.rs", cx)); -// assert!( -// panel.confirm(&Confirm, cx).is_none(), -// "Should not allow to confirm on conflicting new file name" -// ) -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " first.rs", -// " second.rs", -// " third.rs" -// ], -// "File list should be unchanged after failed file create confirmation" -// ); - -// select_path(&panel, "src/test/first.rs", cx); -// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " first.rs <== selected", -// " second.rs", -// " third.rs" -// ], -// ); -// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); -// window.read_with(cx, |cx| { -// let panel = panel.read(cx); -// assert!(panel.filename_editor.is_focused(cx)); -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " [EDITOR: 'first.rs'] <== selected", -// " second.rs", -// " third.rs" -// ] -// ); -// panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("second.rs", cx)); -// assert!( -// panel.confirm(&Confirm, cx).is_none(), -// "Should not allow to confirm on conflicting file rename" -// ) -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " first.rs <== selected", -// " second.rs", -// " third.rs" -// ], -// "File list should be unchanged after failed rename confirmation" -// ); -// } - -// #[gpui::test] -// async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) { -// init_test_with_editor(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// let new_search_events_count = Arc::new(AtomicUsize::new(0)); -// let _subscription = panel.update(cx, |_, cx| { -// let subcription_count = Arc::clone(&new_search_events_count); -// cx.subscribe(&cx.handle(), move |_, _, event, _| { -// if matches!(event, Event::NewSearchInDirectory { .. }) { -// subcription_count.fetch_add(1, atomic::Ordering::SeqCst); -// } -// }) -// }); - -// toggle_expand_dir(&panel, "src/test", cx); -// select_path(&panel, "src/test/first.rs", cx); -// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " first.rs <== selected", -// " second.rs", -// " third.rs" -// ] -// ); -// panel.update(cx, |panel, cx| { -// panel.new_search_in_directory(&NewSearchInDirectory, cx) -// }); -// assert_eq!( -// new_search_events_count.load(atomic::Ordering::SeqCst), -// 0, -// "Should not trigger new search in directory when called on a file" -// ); - -// select_path(&panel, "src/test", cx); -// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test <== selected", -// " first.rs", -// " second.rs", -// " third.rs" -// ] -// ); -// panel.update(cx, |panel, cx| { -// panel.new_search_in_directory(&NewSearchInDirectory, cx) -// }); -// assert_eq!( -// new_search_events_count.load(atomic::Ordering::SeqCst), -// 1, -// "Should trigger new search in directory when called on a directory" -// ); -// } - -// #[gpui::test] -// async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { -// init_test_with_editor(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/project_root", -// json!({ -// "dir_1": { -// "nested_dir": { -// "file_a.py": "# File contents", -// "file_b.py": "# File contents", -// "file_c.py": "# File contents", -// }, -// "file_1.py": "# File contents", -// "file_2.py": "# File contents", -// "file_3.py": "# File contents", -// }, -// "dir_2": { -// "file_1.py": "# File contents", -// "file_2.py": "# File contents", -// "file_3.py": "# File contents", -// } -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// panel.update(cx, |panel, cx| { -// panel.collapse_all_entries(&CollapseAllEntries, cx) -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v project_root", " > dir_1", " > dir_2",] -// ); - -// // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries -// toggle_expand_dir(&panel, "project_root/dir_1", cx); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v project_root", -// " v dir_1 <== selected", -// " > nested_dir", -// " file_1.py", -// " file_2.py", -// " file_3.py", -// " > dir_2", -// ] -// ); -// } - -// #[gpui::test] -// async fn test_new_file_move(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.as_fake().insert_tree("/root", json!({})).await; -// let project = Project::test(fs, ["/root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// // Make a new buffer with no backing file -// workspace.update(cx, |workspace, cx| { -// Editor::new_file(workspace, &Default::default(), cx) -// }); - -// // "Save as"" the buffer, creating a new backing file for it -// let task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(workspace::SaveIntent::Save, cx) -// }); - -// cx.foreground().run_until_parked(); -// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); -// task.await.unwrap(); - -// // Rename the file -// select_path(&panel, "root/new", cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v root", " new <== selected"] -// ); -// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); -// panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("newer", cx)); -// }); -// panel -// .update(cx, |panel, cx| panel.confirm(&Confirm, cx)) -// .unwrap() -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v root", " newer <== selected"] -// ); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.save_active_item(workspace::SaveIntent::Save, cx) -// }) -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); -// // assert that saving the file doesn't restore "new" -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v root", " newer <== selected"] -// ); -// } - -// fn toggle_expand_dir( -// panel: &View, -// path: impl AsRef, -// cx: &mut TestAppContext, -// ) { -// let path = path.as_ref(); -// panel.update(cx, |panel, cx| { -// for worktree in panel.project.read(cx).worktrees().collect::>() { -// let worktree = worktree.read(cx); -// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { -// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; -// panel.toggle_expanded(entry_id, cx); -// return; -// } -// } -// panic!("no worktree for path {:?}", path); -// }); -// } - -// fn select_path(panel: &View, path: impl AsRef, cx: &mut TestAppContext) { -// let path = path.as_ref(); -// panel.update(cx, |panel, cx| { -// for worktree in panel.project.read(cx).worktrees().collect::>() { -// let worktree = worktree.read(cx); -// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { -// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; -// panel.selection = Some(Selection { -// worktree_id: worktree.id(), -// entry_id, -// }); -// return; -// } -// } -// panic!("no worktree for path {:?}", path); -// }); -// } - -// fn visible_entries_as_strings( -// panel: &View, -// range: Range, -// cx: &mut TestAppContext, -// ) -> Vec { -// let mut result = Vec::new(); -// let mut project_entries = HashSet::new(); -// let mut has_editor = false; - -// panel.update(cx, |panel, cx| { -// panel.for_each_visible_entry(range, cx, |project_entry, details, _| { -// if details.is_editing { -// assert!(!has_editor, "duplicate editor entry"); -// has_editor = true; -// } else { -// assert!( -// project_entries.insert(project_entry), -// "duplicate project entry {:?} {:?}", -// project_entry, -// details -// ); -// } - -// let indent = " ".repeat(details.depth); -// let icon = if details.kind.is_dir() { -// if details.is_expanded { -// "v " -// } else { -// "> " -// } -// } else { -// " " -// }; -// let name = if details.is_editing { -// format!("[EDITOR: '{}']", details.filename) -// } else if details.is_processing { -// format!("[PROCESSING: '{}']", details.filename) -// } else { -// details.filename.clone() -// }; -// let selected = if details.is_selected { -// " <== selected" -// } else { -// "" -// }; -// result.push(format!("{indent}{icon}{name}{selected}")); -// }); -// }); - -// result -// } - -// fn init_test(cx: &mut TestAppContext) { -// cx.foreground().forbid_parking(); -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// init_settings(cx); -// theme::init(cx); -// language::init(cx); -// editor::init_settings(cx); -// crate::init((), cx); -// workspace::init_settings(cx); -// client::init_settings(cx); -// Project::init_settings(cx); -// }); -// } - -// fn init_test_with_editor(cx: &mut TestAppContext) { -// cx.foreground().forbid_parking(); -// cx.update(|cx| { -// let app_state = AppState::test(cx); -// theme::init(cx); -// init_settings(cx); -// language::init(cx); -// editor::init(cx); -// pane::init(cx); -// crate::init((), cx); -// workspace::init(app_state.clone(), cx); -// Project::init_settings(cx); -// }); -// } - -// fn ensure_single_file_is_opened( -// window: WindowHandle, -// expected_path: &str, -// cx: &mut TestAppContext, -// ) { -// window.update_root(cx, |workspace, cx| { -// let worktrees = workspace.worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1); -// let worktree_id = WorktreeId::from_usize(worktrees[0].id()); - -// let open_project_paths = workspace -// .panes() -// .iter() -// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) -// .collect::>(); -// assert_eq!( -// open_project_paths, -// vec![ProjectPath { -// worktree_id, -// path: Arc::from(Path::new(expected_path)) -// }], -// "Should have opened file, selected in project panel" -// ); -// }); -// } - -// fn submit_deletion( -// window: AnyWindowHandle, -// panel: &View, -// cx: &mut TestAppContext, -// ) { -// assert!( -// !window.has_pending_prompt(cx), -// "Should have no prompts before the deletion" -// ); -// panel.update(cx, |panel, cx| { -// panel -// .delete(&Delete, cx) -// .expect("Deletion start") -// .detach_and_log_err(cx); -// }); -// assert!( -// window.has_pending_prompt(cx), -// "Should have a prompt after the deletion" -// ); -// window.simulate_prompt_answer(0, cx); -// assert!( -// !window.has_pending_prompt(cx), -// "Should have no prompts after prompt was replied to" -// ); -// cx.foreground().run_until_parked(); -// } - -// fn ensure_no_open_items_and_panes( -// window: AnyWindowHandle, -// workspace: &View, -// cx: &mut TestAppContext, -// ) { -// assert!( -// !window.has_pending_prompt(cx), -// "Should have no prompts after deletion operation closes the file" -// ); -// window.read_with(cx, |cx| { -// let open_project_paths = workspace -// .read(cx) -// .panes() -// .iter() -// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) -// .collect::>(); -// assert!( -// open_project_paths.is_empty(), -// "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" -// ); -// }); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use gpui::{TestAppContext, View, VisualTestContext, WindowHandle}; + use pretty_assertions::assert_eq; + use project::FakeFs; + use serde_json::json; + use settings::SettingsStore; + use std::{ + collections::HashSet, + path::{Path, PathBuf}, + sync::atomic::{self, AtomicUsize}, + }; + use workspace::{pane, AppState}; + + #[gpui::test] + async fn test_visible_list(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root1", + json!({ + ".dockerignore": "", + ".git": { + "HEAD": "", + }, + "a": { + "0": { "q": "", "r": "", "s": "" }, + "1": { "t": "", "u": "" }, + "2": { "v": "", "w": "", "x": "", "y": "" }, + }, + "b": { + "3": { "Q": "" }, + "4": { "R": "", "S": "", "T": "", "U": "" }, + }, + "C": { + "5": {}, + "6": { "V": "", "W": "" }, + "7": { "X": "" }, + "8": { "Y": {}, "Z": "" } + } + }), + ) + .await; + fs.insert_tree( + "/root2", + json!({ + "d": { + "9": "" + }, + "e": {} + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..50, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + toggle_expand_dir(&panel, "root1/b", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..50, cx), + &[ + "v root1", + " > .git", + " > a", + " v b <== selected", + " > 3", + " > 4", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + assert_eq!( + visible_entries_as_strings(&panel, 6..9, cx), + &[ + // + " > C", + " .dockerignore", + "v root2", + ] + ); + } + + #[gpui::test(iterations = 30)] + async fn test_editing_files(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root1", + json!({ + ".dockerignore": "", + ".git": { + "HEAD": "", + }, + "a": { + "0": { "q": "", "r": "", "s": "" }, + "1": { "t": "", "u": "" }, + "2": { "v": "", "w": "", "x": "", "y": "" }, + }, + "b": { + "3": { "Q": "" }, + "4": { "R": "", "S": "", "T": "", "U": "" }, + }, + "C": { + "5": {}, + "6": { "V": "", "W": "" }, + "7": { "X": "" }, + "8": { "Y": {}, "Z": "" } + } + }), + ) + .await; + fs.insert_tree( + "/root2", + json!({ + "d": { + "9": "" + }, + "e": {} + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + select_path(&panel, "root1", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1 <== selected", + " > .git", + " > a", + " > b", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + // Add a file with the root folder selected. The filename editor is placed + // before the first file in the root folder. + panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " [EDITOR: ''] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + let confirm = panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); + panel.confirm_edit(cx).unwrap() + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " [PROCESSING: 'the-new-filename'] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " .dockerignore", + " the-new-filename <== selected", + "v root2", + " > d", + " > e", + ] + ); + + select_path(&panel, "root1/b", cx); + panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " [EDITOR: ''] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + panel + .update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx)); + panel.confirm_edit(cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " another-filename.txt <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + select_path(&panel, "root1/b/another-filename.txt", cx); + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " [EDITOR: 'another-filename.txt'] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + let confirm = panel.update(cx, |panel, cx| { + panel.filename_editor.update(cx, |editor, cx| { + let file_name_selections = editor.selections.all::(cx); + assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); + let file_name_selection = &file_name_selections[0]; + assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); + assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension"); + + editor.set_text("a-different-filename.tar.gz", cx) + }); + panel.confirm_edit(cx).unwrap() + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " [PROCESSING: 'a-different-filename.tar.gz'] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " a-different-filename.tar.gz <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " [EDITOR: 'a-different-filename.tar.gz'] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + panel.update(cx, |panel, cx| { + panel.filename_editor.update(cx, |editor, cx| { + let file_name_selections = editor.selections.all::(cx); + assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); + let file_name_selection = &file_name_selections[0]; + assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); + assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot.."); + + }); + panel.cancel(&Cancel, cx) + }); + + panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > [EDITOR: ''] <== selected", + " > 3", + " > 4", + " a-different-filename.tar.gz", + " > C", + " .dockerignore", + ] + ); + + let confirm = panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("new-dir", cx)); + panel.confirm_edit(cx).unwrap() + }); + panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > [PROCESSING: 'new-dir']", + " > 3 <== selected", + " > 4", + " a-different-filename.tar.gz", + " > C", + " .dockerignore", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3 <== selected", + " > 4", + " > new-dir", + " a-different-filename.tar.gz", + " > C", + " .dockerignore", + ] + ); + + panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > [EDITOR: '3'] <== selected", + " > 4", + " > new-dir", + " a-different-filename.tar.gz", + " > C", + " .dockerignore", + ] + ); + + // Dismiss the rename editor when it loses focus. + workspace.update(cx, |_, cx| cx.blur()).unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3 <== selected", + " > 4", + " > new-dir", + " a-different-filename.tar.gz", + " > C", + " .dockerignore", + ] + ); + } + + #[gpui::test(iterations = 30)] + async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root1", + json!({ + ".dockerignore": "", + ".git": { + "HEAD": "", + }, + "a": { + "0": { "q": "", "r": "", "s": "" }, + "1": { "t": "", "u": "" }, + "2": { "v": "", "w": "", "x": "", "y": "" }, + }, + "b": { + "3": { "Q": "" }, + "4": { "R": "", "S": "", "T": "", "U": "" }, + }, + "C": { + "5": {}, + "6": { "V": "", "W": "" }, + "7": { "X": "" }, + "8": { "Y": {}, "Z": "" } + } + }), + ) + .await; + fs.insert_tree( + "/root2", + json!({ + "d": { + "9": "" + }, + "e": {} + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + select_path(&panel, "root1", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1 <== selected", + " > .git", + " > a", + " > b", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + // Add a file with the root folder selected. The filename editor is placed + // before the first file in the root folder. + panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " [EDITOR: ''] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + let confirm = panel.update(cx, |panel, cx| { + panel.filename_editor.update(cx, |editor, cx| { + editor.set_text("/bdir1/dir2/the-new-filename", cx) + }); + panel.confirm_edit(cx).unwrap() + }); + + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..13, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " v bdir1", + " v dir2", + " the-new-filename <== selected", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + } + + #[gpui::test] + async fn test_copy_paste(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root1", + json!({ + "one.two.txt": "", + "one.txt": "" + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + panel.update(cx, |panel, cx| { + panel.select_next(&Default::default(), cx); + panel.select_next(&Default::default(), cx); + }); + + assert_eq!( + visible_entries_as_strings(&panel, 0..50, cx), + &[ + // + "v root1", + " one.two.txt <== selected", + " one.txt", + ] + ); + + // Regression test - file name is created correctly when + // the copied file's name contains multiple dots. + panel.update(cx, |panel, cx| { + panel.copy(&Default::default(), cx); + panel.paste(&Default::default(), cx); + }); + cx.executor().run_until_parked(); + + assert_eq!( + visible_entries_as_strings(&panel, 0..50, cx), + &[ + // + "v root1", + " one.two copy.txt", + " one.two.txt <== selected", + " one.txt", + ] + ); + + panel.update(cx, |panel, cx| { + panel.paste(&Default::default(), cx); + }); + cx.executor().run_until_parked(); + + assert_eq!( + visible_entries_as_strings(&panel, 0..50, cx), + &[ + // + "v root1", + " one.two copy 1.txt", + " one.two copy.txt", + " one.two.txt <== selected", + " one.txt", + ] + ); + } + + #[gpui::test] + async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + toggle_expand_dir(&panel, "src/test", cx); + select_path(&panel, "src/test/first.rs", cx); + panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs <== selected", + " second.rs", + " third.rs" + ] + ); + ensure_single_file_is_opened(&workspace, "test/first.rs", cx); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " second.rs", + " third.rs" + ], + "Project panel should have no deleted file, no other file is selected in it" + ); + ensure_no_open_items_and_panes(&workspace, cx); + + select_path(&panel, "src/test/second.rs", cx); + panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " second.rs <== selected", + " third.rs" + ] + ); + ensure_single_file_is_opened(&workspace, "test/second.rs", cx); + + workspace + .update(cx, |workspace, cx| { + let active_items = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()) + .collect::>(); + assert_eq!(active_items.len(), 1); + let open_editor = active_items + .into_iter() + .next() + .unwrap() + .downcast::() + .expect("Open item should be an editor"); + open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); + }) + .unwrap(); + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v src", " v test", " third.rs"], + "Project panel should have no deleted file, with one last file remaining" + ); + ensure_no_open_items_and_panes(&workspace, cx); + } + + #[gpui::test] + async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + select_path(&panel, "src/", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v src <== selected", " > test"] + ); + panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v src", " > [EDITOR: ''] <== selected", " > test"] + ); + panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("test", cx)); + assert!( + panel.confirm_edit(cx).is_none(), + "Should not allow to confirm on conflicting new directory name" + ) + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v src", " > test"], + "File list should be unchanged after failed folder create confirmation" + ); + + select_path(&panel, "src/test/", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v src", " > test <== selected"] + ); + panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " [EDITOR: ''] <== selected", + " first.rs", + " second.rs", + " third.rs" + ] + ); + panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("first.rs", cx)); + assert!( + panel.confirm_edit(cx).is_none(), + "Should not allow to confirm on conflicting new file name" + ) + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs", + " second.rs", + " third.rs" + ], + "File list should be unchanged after failed file create confirmation" + ); + + select_path(&panel, "src/test/first.rs", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs <== selected", + " second.rs", + " third.rs" + ], + ); + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " [EDITOR: 'first.rs'] <== selected", + " second.rs", + " third.rs" + ] + ); + panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("second.rs", cx)); + assert!( + panel.confirm_edit(cx).is_none(), + "Should not allow to confirm on conflicting file rename" + ) + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs <== selected", + " second.rs", + " third.rs" + ], + "File list should be unchanged after failed rename confirmation" + ); + } + + #[gpui::test] + async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + let new_search_events_count = Arc::new(AtomicUsize::new(0)); + let _subscription = panel.update(cx, |_, cx| { + let subcription_count = Arc::clone(&new_search_events_count); + let view = cx.view().clone(); + cx.subscribe(&view, move |_, _, event, _| { + if matches!(event, Event::NewSearchInDirectory { .. }) { + subcription_count.fetch_add(1, atomic::Ordering::SeqCst); + } + }) + }); + + toggle_expand_dir(&panel, "src/test", cx); + select_path(&panel, "src/test/first.rs", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs <== selected", + " second.rs", + " third.rs" + ] + ); + panel.update(cx, |panel, cx| { + panel.new_search_in_directory(&NewSearchInDirectory, cx) + }); + assert_eq!( + new_search_events_count.load(atomic::Ordering::SeqCst), + 0, + "Should not trigger new search in directory when called on a file" + ); + + select_path(&panel, "src/test", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test <== selected", + " first.rs", + " second.rs", + " third.rs" + ] + ); + panel.update(cx, |panel, cx| { + panel.new_search_in_directory(&NewSearchInDirectory, cx) + }); + assert_eq!( + new_search_events_count.load(atomic::Ordering::SeqCst), + 1, + "Should trigger new search in directory when called on a directory" + ); + } + + #[gpui::test] + async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/project_root", + json!({ + "dir_1": { + "nested_dir": { + "file_a.py": "# File contents", + "file_b.py": "# File contents", + "file_c.py": "# File contents", + }, + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + }, + "dir_2": { + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + panel.update(cx, |panel, cx| { + panel.collapse_all_entries(&CollapseAllEntries, cx) + }); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v project_root", " > dir_1", " > dir_2",] + ); + + // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries + toggle_expand_dir(&panel, "project_root/dir_1", cx); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v project_root", + " v dir_1 <== selected", + " > nested_dir", + " file_1.py", + " file_2.py", + " file_3.py", + " > dir_2", + ] + ); + } + + #[gpui::test] + async fn test_new_file_move(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.as_fake().insert_tree("/root", json!({})).await; + let project = Project::test(fs, ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + // Make a new buffer with no backing file + workspace + .update(cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .unwrap(); + + // "Save as"" the buffer, creating a new backing file for it + workspace + .update(cx, |workspace, cx| { + workspace.save_active_item(workspace::SaveIntent::Save, cx) + }) + .unwrap() + .await + .unwrap(); + + cx.executor().run_until_parked(); + cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); + cx.executor().run_until_parked(); + + // Rename the file + select_path(&panel, "root/new", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v root", " new <== selected"] + ); + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("newer", cx)); + }); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v root", " newer <== selected"] + ); + + workspace + .update(cx, |workspace, cx| { + workspace.save_active_item(workspace::SaveIntent::Save, cx) + }) + .unwrap() + .await + .unwrap(); + + cx.executor().run_until_parked(); + // assert that saving the file doesn't restore "new" + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v root", " newer <== selected"] + ); + } + + fn toggle_expand_dir( + panel: &View, + path: impl AsRef, + cx: &mut VisualTestContext, + ) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for worktree in panel.project.read(cx).worktrees().collect::>() { + let worktree = worktree.read(cx); + if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { + let entry_id = worktree.entry_for_path(relative_path).unwrap().id; + panel.toggle_expanded(entry_id, cx); + return; + } + } + panic!("no worktree for path {:?}", path); + }); + } + + fn select_path(panel: &View, path: impl AsRef, cx: &mut VisualTestContext) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for worktree in panel.project.read(cx).worktrees().collect::>() { + let worktree = worktree.read(cx); + if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { + let entry_id = worktree.entry_for_path(relative_path).unwrap().id; + panel.selection = Some(Selection { + worktree_id: worktree.id(), + entry_id, + }); + return; + } + } + panic!("no worktree for path {:?}", path); + }); + } + + fn visible_entries_as_strings( + panel: &View, + range: Range, + cx: &mut VisualTestContext, + ) -> Vec { + let mut result = Vec::new(); + let mut project_entries = HashSet::new(); + let mut has_editor = false; + + panel.update(cx, |panel, cx| { + panel.for_each_visible_entry(range, cx, |project_entry, details, _| { + if details.is_editing { + assert!(!has_editor, "duplicate editor entry"); + has_editor = true; + } else { + assert!( + project_entries.insert(project_entry), + "duplicate project entry {:?} {:?}", + project_entry, + details + ); + } + + let indent = " ".repeat(details.depth); + let icon = if details.kind.is_dir() { + if details.is_expanded { + "v " + } else { + "> " + } + } else { + " " + }; + let name = if details.is_editing { + format!("[EDITOR: '{}']", details.filename) + } else if details.is_processing { + format!("[PROCESSING: '{}']", details.filename) + } else { + details.filename.clone() + }; + let selected = if details.is_selected { + " <== selected" + } else { + "" + }; + result.push(format!("{indent}{icon}{name}{selected}")); + }); + }); + + result + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + init_settings(cx); + theme::init(cx); + language::init(cx); + editor::init_settings(cx); + crate::init((), cx); + workspace::init_settings(cx); + client::init_settings(cx); + Project::init_settings(cx); + }); + } + + fn init_test_with_editor(cx: &mut TestAppContext) { + cx.update(|cx| { + let app_state = AppState::test(cx); + theme::init(cx); + init_settings(cx); + language::init(cx); + editor::init(cx); + pane::init(cx); + crate::init((), cx); + workspace::init(app_state.clone(), cx); + Project::init_settings(cx); + }); + } + + fn ensure_single_file_is_opened( + window: &WindowHandle, + expected_path: &str, + cx: &mut TestAppContext, + ) { + window + .update(cx, |workspace, cx| { + let worktrees = workspace.worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + let worktree_id = worktrees[0].read(cx).id(); + + let open_project_paths = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) + .collect::>(); + assert_eq!( + open_project_paths, + vec![ProjectPath { + worktree_id, + path: Arc::from(Path::new(expected_path)) + }], + "Should have opened file, selected in project panel" + ); + }) + .unwrap(); + } + + fn submit_deletion(panel: &View, cx: &mut VisualTestContext) { + assert!( + !cx.has_pending_prompt(), + "Should have no prompts before the deletion" + ); + panel.update(cx, |panel, cx| panel.delete(&Delete, cx)); + assert!( + cx.has_pending_prompt(), + "Should have a prompt after the deletion" + ); + cx.simulate_prompt_answer(0); + assert!( + !cx.has_pending_prompt(), + "Should have no prompts after prompt was replied to" + ); + cx.executor().run_until_parked(); + } + + fn ensure_no_open_items_and_panes( + workspace: &WindowHandle, + cx: &mut VisualTestContext, + ) { + assert!( + !cx.has_pending_prompt(), + "Should have no prompts after deletion operation closes the file" + ); + workspace + .read_with(cx, |workspace, cx| { + let open_project_paths = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) + .collect::>(); + assert!( + open_project_paths.is_empty(), + "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" + ); + }) + .unwrap(); + } +} From 3b01a032ba22ab863773b6e1c9f214b0594d723b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 14:38:23 -0700 Subject: [PATCH 111/126] In the middle of stuff --- crates/editor2/src/element.rs | 1 + crates/file_finder2/src/file_finder.rs | 2693 +++++++++++------------- crates/gpui2/src/app/test_context.rs | 34 +- crates/gpui2/src/window.rs | 3 + crates/workspace2/src/workspace2.rs | 20 +- 5 files changed, 1278 insertions(+), 1473 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 638ed33891..a68825fa77 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1448,6 +1448,7 @@ impl EditorElement { let snapshot = editor.snapshot(cx); let style = self.style.clone(); + dbg!(&style.text.font()); let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); let font_size = style.text.font_size.to_pixels(cx.rem_size()); let line_height = style.text.line_height_in_pixels(cx.rem_size()); diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 13296887cb..c460cac252 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -32,9 +32,9 @@ pub fn init(cx: &mut AppContext) { impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { - dbg!("yay"); + dbg!("REGISTERING"); workspace.register_action(|workspace, _: &Toggle, cx| { - dbg!("yayer"); + dbg!("CALLING ACTION"); let Some(file_finder) = workspace.current_modal::(cx) else { Self::open(workspace, cx); return; @@ -738,1459 +738,1236 @@ impl PickerDelegate for FileFinderDelegate { } } -// #[cfg(test)] -// mod tests { -// use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; - -// use super::*; -// use editor::Editor; -// use gpui::{TestAppContext, ViewHandle}; -// use menu::{Confirm, SelectNext}; -// use serde_json::json; -// use workspace::{AppState, Workspace}; - -// #[ctor::ctor] -// fn init_logger() { -// if std::env::var("RUST_LOG").is_ok() { -// env_logger::init(); -// } -// } - -// #[gpui::test] -// async fn test_matching_paths(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "banana": "", -// "bandana": "", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// cx.dispatch_action(window.into(), Toggle); - -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.delegate_mut().update_matches("bna".to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// assert_eq!(finder.delegate().matches.len(), 2); -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(window.into(), SelectNext); -// cx.dispatch_action(window.into(), Confirm); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// cx.read(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// assert_eq!( -// active_item -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx), -// "bandana" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { -// let app_state = init_test(cx); - -// let first_file_name = "first.rs"; -// let first_file_contents = "// First Rust file"; -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// first_file_name: first_file_contents, -// "second.rs": "// Second Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// cx.dispatch_action(window.into(), Toggle); -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - -// let file_query = &first_file_name[..3]; -// let file_row = 1; -// let file_column = 3; -// assert!(file_column <= first_file_contents.len()); -// let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate_mut() -// .update_matches(query_inside_file.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let finder = finder.delegate(); -// assert_eq!(finder.matches.len(), 1); -// let latest_search_query = finder -// .latest_search_query -// .as_ref() -// .expect("Finder should have a query after the update_matches call"); -// assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); -// assert_eq!( -// latest_search_query.path_like.file_query_end, -// Some(file_query.len()) -// ); -// assert_eq!(latest_search_query.row, Some(file_row)); -// assert_eq!(latest_search_query.column, Some(file_column as u32)); -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(window.into(), SelectNext); -// cx.dispatch_action(window.into(), Confirm); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// let editor = cx.update(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// active_item.downcast::().unwrap() -// }); -// cx.foreground().advance_clock(Duration::from_secs(2)); -// cx.foreground().start_waiting(); -// cx.foreground().finish_waiting(); -// editor.update(cx, |editor, cx| { -// let all_selections = editor.selections.all_adjusted(cx); -// assert_eq!( -// all_selections.len(), -// 1, -// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" -// ); -// let caret_selection = all_selections.into_iter().next().unwrap(); -// assert_eq!(caret_selection.start, caret_selection.end, -// "Caret selection should have its start and end at the same position"); -// assert_eq!(file_row, caret_selection.start.row + 1, -// "Query inside file should get caret with the same focus row"); -// assert_eq!(file_column, caret_selection.start.column as usize + 1, -// "Query inside file should get caret with the same focus column"); -// }); -// } - -// #[gpui::test] -// async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { -// let app_state = init_test(cx); - -// let first_file_name = "first.rs"; -// let first_file_contents = "// First Rust file"; -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// first_file_name: first_file_contents, -// "second.rs": "// Second Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// cx.dispatch_action(window.into(), Toggle); -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - -// let file_query = &first_file_name[..3]; -// let file_row = 200; -// let file_column = 300; -// assert!(file_column > first_file_contents.len()); -// let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate_mut() -// .update_matches(query_outside_file.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let finder = finder.delegate(); -// assert_eq!(finder.matches.len(), 1); -// let latest_search_query = finder -// .latest_search_query -// .as_ref() -// .expect("Finder should have a query after the update_matches call"); -// assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); -// assert_eq!( -// latest_search_query.path_like.file_query_end, -// Some(file_query.len()) -// ); -// assert_eq!(latest_search_query.row, Some(file_row)); -// assert_eq!(latest_search_query.column, Some(file_column as u32)); -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(window.into(), SelectNext); -// cx.dispatch_action(window.into(), Confirm); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// let editor = cx.update(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// active_item.downcast::().unwrap() -// }); -// cx.foreground().advance_clock(Duration::from_secs(2)); -// cx.foreground().start_waiting(); -// cx.foreground().finish_waiting(); -// editor.update(cx, |editor, cx| { -// let all_selections = editor.selections.all_adjusted(cx); -// assert_eq!( -// all_selections.len(), -// 1, -// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" -// ); -// let caret_selection = all_selections.into_iter().next().unwrap(); -// assert_eq!(caret_selection.start, caret_selection.end, -// "Caret selection should have its start and end at the same position"); -// assert_eq!(0, caret_selection.start.row, -// "Excessive rows (as in query outside file borders) should get trimmed to last file row"); -// assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, -// "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); -// }); -// } - -// #[gpui::test] -// async fn test_matching_cancellation(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/dir", -// json!({ -// "hello": "", -// "goodbye": "", -// "halogen-light": "", -// "happiness": "", -// "height": "", -// "hi": "", -// "hiccup": "", -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// None, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); - -// let query = test_path_like("hi"); -// finder -// .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) -// .await; -// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5)); - -// finder.update(cx, |finder, cx| { -// let delegate = finder.delegate_mut(); -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// let matches = delegate.matches.search.clone(); - -// // Simulate a search being cancelled after the time limit, -// // returning only a subset of the matches that would have been found. -// drop(delegate.spawn_search(query.clone(), cx)); -// delegate.set_search_matches( -// delegate.latest_search_id, -// true, // did-cancel -// query.clone(), -// vec![matches[1].clone(), matches[3].clone()], -// cx, -// ); - -// // Simulate another cancellation. -// drop(delegate.spawn_search(query.clone(), cx)); -// delegate.set_search_matches( -// delegate.latest_search_id, -// true, // did-cancel -// query.clone(), -// vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], -// cx, -// ); - -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); -// }); -// } - -// #[gpui::test] -// async fn test_ignored_files(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/ancestor", -// json!({ -// ".gitignore": "ignored-root", -// "ignored-root": { -// "happiness": "", -// "height": "", -// "hi": "", -// "hiccup": "", -// }, -// "tracked-root": { -// ".gitignore": "height", -// "happiness": "", -// "height": "", -// "hi": "", -// "hiccup": "", -// }, -// }), -// ) -// .await; - -// let project = Project::test( -// app_state.fs.clone(), -// [ -// "/ancestor/tracked-root".as_ref(), -// "/ancestor/ignored-root".as_ref(), -// ], -// cx, -// ) -// .await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// None, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); -// finder -// .update(cx, |f, cx| { -// f.delegate_mut().spawn_search(test_path_like("hi"), cx) -// }) -// .await; -// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); -// } - -// #[gpui::test] -// async fn test_single_file_worktrees(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) -// .await; - -// let project = Project::test( -// app_state.fs.clone(), -// ["/root/the-parent-dir/the-file".as_ref()], -// cx, -// ) -// .await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// None, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); - -// // Even though there is only one worktree, that worktree's filename -// // is included in the matching, because the worktree is a single file. -// finder -// .update(cx, |f, cx| { -// f.delegate_mut().spawn_search(test_path_like("thf"), cx) -// }) -// .await; -// cx.read(|cx| { -// let finder = finder.read(cx); -// let delegate = finder.delegate(); -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// let matches = delegate.matches.search.clone(); -// assert_eq!(matches.len(), 1); - -// let (file_name, file_name_positions, full_path, full_path_positions) = -// delegate.labels_for_path_match(&matches[0]); -// assert_eq!(file_name, "the-file"); -// assert_eq!(file_name_positions, &[0, 1, 4]); -// assert_eq!(full_path, "the-file"); -// assert_eq!(full_path_positions, &[0, 1, 4]); -// }); - -// // Since the worktree root is a file, searching for its name followed by a slash does -// // not match anything. -// finder -// .update(cx, |f, cx| { -// f.delegate_mut().spawn_search(test_path_like("thf/"), cx) -// }) -// .await; -// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); -// } - -// #[gpui::test] -// async fn test_path_distance_ordering(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "dir1": { "a.txt": "" }, -// "dir2": { -// "a.txt": "", -// "b.txt": "" -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1); -// WorktreeId::from_usize(worktrees[0].id()) -// }); - -// // When workspace has an active item, sort items which are closer to that item -// // first when they have the same name. In this case, b.txt is closer to dir2's a.txt -// // so that one should be sorted earlier -// let b_path = Some(dummy_found_path(ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("/root/dir2/b.txt")), -// })); -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// b_path, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); - -// finder -// .update(cx, |f, cx| { -// f.delegate_mut().spawn_search(test_path_like("a.txt"), cx) -// }) -// .await; - -// finder.read_with(cx, |f, _| { -// let delegate = f.delegate(); -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// let matches = delegate.matches.search.clone(); -// assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); -// assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); -// }); -// } - -// #[gpui::test] -// async fn test_search_worktree_without_files(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "dir1": {}, -// "dir2": { -// "dir3": {} -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// None, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); -// finder -// .update(cx, |f, cx| { -// f.delegate_mut().spawn_search(test_path_like("dir"), cx) -// }) -// .await; -// cx.read(|cx| { -// let finder = finder.read(cx); -// assert_eq!(finder.delegate().matches.len(), 0); -// }); -// } - -// #[gpui::test] -// async fn test_query_history( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1); -// WorktreeId::from_usize(worktrees[0].id()) -// }); - -// // Open and close panels, getting their history items afterwards. -// // Ensure history items get populated with opened items, and items are kept in a certain order. -// // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. -// // -// // TODO: without closing, the opened items do not propagate their history changes for some reason -// // it does work in real app though, only tests do not propagate. - -// let initial_history = open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert!( -// initial_history.is_empty(), -// "Should have no history before opening any files" -// ); - -// let history_after_first = open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// history_after_first, -// vec![FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// )], -// "Should show 1st opened item in the history when opening the 2nd item" -// ); - -// let history_after_second = open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// history_after_second, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// ), -// ], -// "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ -// 2nd item should be the first in the history, as the last opened." -// ); - -// let history_after_third = open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// history_after_third, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/third.rs")), -// }, -// Some(PathBuf::from("/src/test/third.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// ), -// ], -// "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ -// 3rd item should be the first in the history, as the last opened." -// ); - -// let history_after_second_again = open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// history_after_second_again, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/third.rs")), -// }, -// Some(PathBuf::from("/src/test/third.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// ), -// ], -// "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ -// 2nd item, as the last opened, 3rd item should go next as it was opened right before." -// ); -// } - -// #[gpui::test] -// async fn test_external_files_history( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// } -// }), -// ) -// .await; - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/external-src", -// json!({ -// "test": { -// "third.rs": "// Third Rust file", -// "fourth.rs": "// Fourth Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// cx.update(|cx| { -// project.update(cx, |project, cx| { -// project.find_or_create_local_worktree("/external-src", false, cx) -// }) -// }) -// .detach(); -// deterministic.run_until_parked(); - -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1,); - -// WorktreeId::from_usize(worktrees[0].id()) -// }); -// workspace -// .update(cx, |workspace, cx| { -// workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) -// }) -// .detach(); -// deterministic.run_until_parked(); -// let external_worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!( -// worktrees.len(), -// 2, -// "External file should get opened in a new worktree" -// ); - -// WorktreeId::from_usize( -// worktrees -// .into_iter() -// .find(|worktree| worktree.id() != worktree_id.to_usize()) -// .expect("New worktree should have a different id") -// .id(), -// ) -// }); -// close_active_item(&workspace, &deterministic, cx).await; - -// let initial_history_items = open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// initial_history_items, -// vec![FoundPath::new( -// ProjectPath { -// worktree_id: external_worktree_id, -// path: Arc::from(Path::new("")), -// }, -// Some(PathBuf::from("/external-src/test/third.rs")) -// )], -// "Should show external file with its full path in the history after it was open" -// ); - -// let updated_history_items = open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// updated_history_items, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id: external_worktree_id, -// path: Arc::from(Path::new("")), -// }, -// Some(PathBuf::from("/external-src/test/third.rs")) -// ), -// ], -// "Should keep external file with history updates", -// ); -// } - -// #[gpui::test] -// async fn test_toggle_panel_new_selections( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// // generate some history to select from -// open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// let current_history = open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; - -// for expected_selected_index in 0..current_history.len() { -// cx.dispatch_action(window.into(), Toggle); -// let selected_index = cx.read(|cx| { -// workspace -// .read(cx) -// .modal::() -// .unwrap() -// .read(cx) -// .delegate() -// .selected_index() -// }); -// assert_eq!( -// selected_index, expected_selected_index, -// "Should select the next item in the history" -// ); -// } - -// cx.dispatch_action(window.into(), Toggle); -// let selected_index = cx.read(|cx| { -// workspace -// .read(cx) -// .modal::() -// .unwrap() -// .read(cx) -// .delegate() -// .selected_index() -// }); -// assert_eq!( -// selected_index, 0, -// "Should wrap around the history and start all over" -// ); -// } - -// #[gpui::test] -// async fn test_search_preserves_history_items( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// "fourth.rs": "// Fourth Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1,); - -// WorktreeId::from_usize(worktrees[0].id()) -// }); - -// // generate some history to select from -// open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; - -// cx.dispatch_action(window.into(), Toggle); -// let first_query = "f"; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate_mut() -// .update_matches(first_query.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = finder.delegate(); -// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); -// let history_match = delegate.matches.history.first().unwrap(); -// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); -// assert_eq!(history_match.0, FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// )); -// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); -// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); -// }); - -// let second_query = "fsdasdsa"; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate_mut() -// .update_matches(second_query.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = finder.delegate(); -// assert!( -// delegate.matches.history.is_empty(), -// "No history entries should match {second_query}" -// ); -// assert!( -// delegate.matches.search.is_empty(), -// "No search entries should match {second_query}" -// ); -// }); - -// let first_query_again = first_query; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate_mut() -// .update_matches(first_query_again.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = finder.delegate(); -// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); -// let history_match = delegate.matches.history.first().unwrap(); -// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); -// assert_eq!(history_match.0, FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// )); -// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); -// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); -// }); -// } - -// #[gpui::test] -// async fn test_history_items_vs_very_good_external_match( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "collab_ui": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// "collab_ui.rs": "// Fourth Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// // generate some history to select from -// open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; - -// cx.dispatch_action(window.into(), Toggle); -// let query = "collab_ui"; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.delegate_mut().update_matches(query.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = finder.delegate(); -// assert!( -// delegate.matches.history.is_empty(), -// "History items should not math query {query}, they should be matched by name only" -// ); - -// let search_entries = delegate -// .matches -// .search -// .iter() -// .map(|path_match| path_match.path.to_path_buf()) -// .collect::>(); -// assert_eq!( -// search_entries, -// vec![ -// PathBuf::from("collab_ui/collab_ui.rs"), -// PathBuf::from("collab_ui/third.rs"), -// PathBuf::from("collab_ui/first.rs"), -// PathBuf::from("collab_ui/second.rs"), -// ], -// "Despite all search results having the same directory name, the most matching one should be on top" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_nonexistent_history_items_not_shown( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "nonexistent.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// // generate some history to select from -// open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "non", -// 1, -// "nonexistent.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; - -// cx.dispatch_action(window.into(), Toggle); -// let query = "rs"; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.delegate_mut().update_matches(query.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = finder.delegate(); -// let history_entries = delegate -// .matches -// .history -// .iter() -// .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) -// .collect::>(); -// assert_eq!( -// history_entries, -// vec![ -// PathBuf::from("test/first.rs"), -// PathBuf::from("test/third.rs"), -// ], -// "Should have all opened files in the history, except the ones that do not exist on disk" -// ); -// }); -// } - -// async fn open_close_queried_buffer( -// input: &str, -// expected_matches: usize, -// expected_editor_title: &str, -// window: gpui::AnyWindowHandle, -// workspace: &ViewHandle, -// deterministic: &gpui::executor::Deterministic, -// cx: &mut gpui::TestAppContext, -// ) -> Vec { -// cx.dispatch_action(window, Toggle); -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.delegate_mut().update_matches(input.to_string(), cx) -// }) -// .await; -// let history_items = finder.read_with(cx, |finder, _| { -// assert_eq!( -// finder.delegate().matches.len(), -// expected_matches, -// "Unexpected number of matches found for query {input}" -// ); -// finder.delegate().history_items.clone() -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(window, SelectNext); -// cx.dispatch_action(window, Confirm); -// deterministic.run_until_parked(); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// cx.read(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// let active_editor_title = active_item -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx); -// assert_eq!( -// expected_editor_title, active_editor_title, -// "Unexpected editor title for query {input}" -// ); -// }); - -// close_active_item(workspace, deterministic, cx).await; - -// history_items -// } - -// async fn close_active_item( -// workspace: &ViewHandle, -// deterministic: &gpui::executor::Deterministic, -// cx: &mut TestAppContext, -// ) { -// let mut original_items = HashMap::new(); -// cx.read(|cx| { -// for pane in workspace.read(cx).panes() { -// let pane_id = pane.id(); -// let pane = pane.read(cx); -// let insertion_result = original_items.insert(pane_id, pane.items().count()); -// assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); -// } -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// active_pane -// .update(cx, |pane, cx| { -// pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) -// .unwrap() -// }) -// .await -// .unwrap(); -// deterministic.run_until_parked(); -// cx.read(|cx| { -// for pane in workspace.read(cx).panes() { -// let pane_id = pane.id(); -// let pane = pane.read(cx); -// match original_items.remove(&pane_id) { -// Some(original_items) => { -// assert_eq!( -// pane.items().count(), -// original_items.saturating_sub(1), -// "Pane id {pane_id} should have item closed" -// ); -// } -// None => panic!("Pane id {pane_id} not found in original items"), -// } -// } -// }); -// assert!( -// original_items.len() <= 1, -// "At most one panel should got closed" -// ); -// } - -// fn init_test(cx: &mut TestAppContext) -> Arc { -// cx.foreground_executor().forbid_parking(); -// cx.update(|cx| { -// let state = AppState::test(cx); -// theme::init(cx); -// language::init(cx); -// super::init(cx); -// editor::init(cx); -// workspace::init_settings(cx); -// Project::init_settings(cx); -// state -// }) -// } - -// fn test_path_like(test_str: &str) -> PathLikeWithPosition { -// PathLikeWithPosition::parse_str(test_str, |path_like_str| { -// Ok::<_, std::convert::Infallible>(FileSearchQuery { -// raw_query: test_str.to_owned(), -// file_query_end: if path_like_str == test_str { -// None -// } else { -// Some(path_like_str.len()) -// }, -// }) -// }) -// .unwrap() -// } - -// fn dummy_found_path(project_path: ProjectPath) -> FoundPath { -// FoundPath { -// project: project_path, -// absolute: None, -// } -// } -// } +#[cfg(test)] +mod tests { + use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; + + use super::*; + use editor::Editor; + use gpui::{Entity, TestAppContext, VisualTestContext}; + use menu::{Confirm, SelectNext}; + use serde_json::json; + use workspace::{AppState, Workspace}; + + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } + + #[gpui::test] + async fn test_matching_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "banana": "", + "bandana": "", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + + let (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + picker + .update(cx, |picker, cx| { + picker.delegate.update_matches("bna".to_string(), cx) + }) + .await; + + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 2); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + cx.read(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + assert_eq!( + active_item + .to_any() + .downcast::() + .unwrap() + .read(cx) + .title(cx), + "bandana" + ); + }); + } + + #[gpui::test] + async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + + let (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + let file_query = &first_file_name[..3]; + let file_row = 1; + let file_column = 3; + assert!(file_column <= first_file_contents.len()); + let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); + picker + .update(cx, |finder, cx| { + finder + .delegate + .update_matches(query_inside_file.to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + let finder = &finder.delegate; + assert_eq!(finder.matches.len(), 1); + let latest_search_query = finder + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + let editor = cx.update(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + active_item.downcast::().unwrap() + }); + cx.executor().advance_clock(Duration::from_secs(2)); + + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(file_row, caret_selection.start.row + 1, + "Query inside file should get caret with the same focus row"); + assert_eq!(file_column, caret_selection.start.column as usize + 1, + "Query inside file should get caret with the same focus column"); + }); + } + + #[gpui::test] + async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + + let (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + let file_query = &first_file_name[..3]; + let file_row = 200; + let file_column = 300; + assert!(file_column > first_file_contents.len()); + let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); + picker + .update(cx, |picker, cx| { + picker + .delegate + .update_matches(query_outside_file.to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + let delegate = &finder.delegate; + assert_eq!(delegate.matches.len(), 1); + let latest_search_query = delegate + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + let editor = cx.update(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + active_item.downcast::().unwrap() + }); + cx.executor().advance_clock(Duration::from_secs(2)); + + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(0, caret_selection.start.row, + "Excessive rows (as in query outside file borders) should get trimmed to last file row"); + assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, + "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); + }); + } + + #[gpui::test] + async fn test_matching_cancellation(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/dir", + json!({ + "hello": "", + "goodbye": "", + "halogen-light": "", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; + + let (picker, _, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + let query = test_path_like("hi"); + picker + .update(cx, |picker, cx| { + picker.delegate.spawn_search(query.clone(), cx) + }) + .await; + + picker.update(cx, |picker, _cx| { + assert_eq!(picker.delegate.matches.len(), 5) + }); + + picker.update(cx, |picker, cx| { + let delegate = &mut picker.delegate; + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); + + // Simulate a search being cancelled after the time limit, + // returning only a subset of the matches that would have been found. + drop(delegate.spawn_search(query.clone(), cx)); + delegate.set_search_matches( + delegate.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[1].clone(), matches[3].clone()], + cx, + ); + + // Simulate another cancellation. + drop(delegate.spawn_search(query.clone(), cx)); + delegate.set_search_matches( + delegate.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], + cx, + ); + + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); + }); + } + + #[gpui::test] + async fn test_ignored_files(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/ancestor", + json!({ + ".gitignore": "ignored-root", + "ignored-root": { + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + "tracked-root": { + ".gitignore": "height", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + }), + ) + .await; + + let project = Project::test( + app_state.fs.clone(), + [ + "/ancestor/tracked-root".as_ref(), + "/ancestor/ignored-root".as_ref(), + ], + cx, + ) + .await; + + let (picker, _, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + picker + .update(cx, |picker, cx| { + picker.delegate.spawn_search(test_path_like("hi"), cx) + }) + .await; + picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7)); + } + + // #[gpui::test] + // async fn test_single_file_worktrees(cx: &mut TestAppContext) { + // let app_state = init_test(cx); + // app_state + // .fs + // .as_fake() + // .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) + // .await; + + // let project = Project::test( + // app_state.fs.clone(), + // ["/root/the-parent-dir/the-file".as_ref()], + // cx, + // ) + // .await; + + // let (picker, _, mut cx) = build_find_picker(project, cx); + // let cx = &mut cx; + + // // Even though there is only one worktree, that worktree's filename + // // is included in the matching, because the worktree is a single file. + // picker + // .update(cx, |picker, cx| { + // picker.delegate.spawn_search(test_path_like("thf"), cx) + // }) + // .await; + // cx.read(|cx| { + // let picker = picker.read(cx); + // let delegate = &picker.delegate; + // assert!( + // delegate.matches.history.is_empty(), + // "Search matches expected" + // ); + // let matches = delegate.matches.search.clone(); + // assert_eq!(matches.len(), 1); + + // let (file_name, file_name_positions, full_path, full_path_positions) = + // delegate.labels_for_path_match(&matches[0]); + // assert_eq!(file_name, "the-file"); + // assert_eq!(file_name_positions, &[0, 1, 4]); + // assert_eq!(full_path, "the-file"); + // assert_eq!(full_path_positions, &[0, 1, 4]); + // }); + + // // Since the worktree root is a file, searching for its name followed by a slash does + // // not match anything. + // picker + // .update(cx, |f, cx| { + // f.delegate.spawn_search(test_path_like("thf/"), cx) + // }) + // .await; + // picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0)); + // } + + // #[gpui::test] + // async fn test_path_distance_ordering(cx: &mut TestAppContext) { + // let app_state = init_test(cx); + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/root", + // json!({ + // "dir1": { "a.txt": "" }, + // "dir2": { + // "a.txt": "", + // "b.txt": "" + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + + // let worktree_id = cx.read(|cx| { + // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + // assert_eq!(worktrees.len(), 1); + // WorktreeId::from_usize(worktrees[0].id()) + // }); + + // // When workspace has an active item, sort items which are closer to that item + // // first when they have the same name. In this case, b.txt is closer to dir2's a.txt + // // so that one should be sorted earlier + // let b_path = Some(dummy_found_path(ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("/root/dir2/b.txt")), + // })); + // cx.dispatch_action(Toggle); + + // let finder = cx + // .add_window(|cx| { + // Picker::new( + // FileFinderDelegate::new( + // workspace.downgrade(), + // workspace.read(cx).project().clone(), + // b_path, + // Vec::new(), + // cx, + // ), + // cx, + // ) + // }) + // .root(cx); + + // finder + // .update(cx, |f, cx| { + // f.delegate.spawn_search(test_path_like("a.txt"), cx) + // }) + // .await; + + // finder.read_with(cx, |f, _| { + // let delegate = &f.delegate; + // assert!( + // delegate.matches.history.is_empty(), + // "Search matches expected" + // ); + // let matches = delegate.matches.search.clone(); + // assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); + // assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); + // }); + // } + + // #[gpui::test] + // async fn test_search_worktree_without_files(cx: &mut TestAppContext) { + // let app_state = init_test(cx); + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/root", + // json!({ + // "dir1": {}, + // "dir2": { + // "dir3": {} + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + // let workspace = cx + // .add_window(|cx| Workspace::test_new(project, cx)) + // .root(cx); + // let finder = cx + // .add_window(|cx| { + // Picker::new( + // FileFinderDelegate::new( + // workspace.downgrade(), + // workspace.read(cx).project().clone(), + // None, + // Vec::new(), + // cx, + // ), + // cx, + // ) + // }) + // .root(cx); + // finder + // .update(cx, |f, cx| { + // f.delegate.spawn_search(test_path_like("dir"), cx) + // }) + // .await; + // cx.read(|cx| { + // let finder = finder.read(cx); + // assert_eq!(finder.delegate.matches.len(), 0); + // }); + // } + + // #[gpui::test] + // async fn test_query_history(cx: &mut gpui::TestAppContext) { + // let app_state = init_test(cx); + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/src", + // json!({ + // "test": { + // "first.rs": "// First Rust file", + // "second.rs": "// Second Rust file", + // "third.rs": "// Third Rust file", + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // let worktree_id = cx.read(|cx| { + // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + // assert_eq!(worktrees.len(), 1); + // WorktreeId::from_usize(worktrees[0].id()) + // }); + + // // Open and close panels, getting their history items afterwards. + // // Ensure history items get populated with opened items, and items are kept in a certain order. + // // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. + // // + // // TODO: without closing, the opened items do not propagate their history changes for some reason + // // it does work in real app though, only tests do not propagate. + + // let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // assert!( + // initial_history.is_empty(), + // "Should have no history before opening any files" + // ); + + // let history_after_first = + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // assert_eq!( + // history_after_first, + // vec![FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // )], + // "Should show 1st opened item in the history when opening the 2nd item" + // ); + + // let history_after_second = + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // assert_eq!( + // history_after_second, + // vec![ + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/second.rs")), + // }, + // Some(PathBuf::from("/src/test/second.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // ), + // ], + // "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ + // 2nd item should be the first in the history, as the last opened." + // ); + + // let history_after_third = + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // assert_eq!( + // history_after_third, + // vec![ + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/third.rs")), + // }, + // Some(PathBuf::from("/src/test/third.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/second.rs")), + // }, + // Some(PathBuf::from("/src/test/second.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // ), + // ], + // "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ + // 3rd item should be the first in the history, as the last opened." + // ); + + // let history_after_second_again = + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // assert_eq!( + // history_after_second_again, + // vec![ + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/second.rs")), + // }, + // Some(PathBuf::from("/src/test/second.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/third.rs")), + // }, + // Some(PathBuf::from("/src/test/third.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // ), + // ], + // "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ + // 2nd item, as the last opened, 3rd item should go next as it was opened right before." + // ); + // } + + // #[gpui::test] + // async fn test_external_files_history(cx: &mut gpui::TestAppContext) { + // let app_state = init_test(cx); + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/src", + // json!({ + // "test": { + // "first.rs": "// First Rust file", + // "second.rs": "// Second Rust file", + // } + // }), + // ) + // .await; + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/external-src", + // json!({ + // "test": { + // "third.rs": "// Third Rust file", + // "fourth.rs": "// Fourth Rust file", + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + // cx.update(|cx| { + // project.update(cx, |project, cx| { + // project.find_or_create_local_worktree("/external-src", false, cx) + // }) + // }) + // .detach(); + // cx.background_executor.run_until_parked(); + + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // let worktree_id = cx.read(|cx| { + // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + // assert_eq!(worktrees.len(), 1,); + + // WorktreeId::from_usize(worktrees[0].id()) + // }); + // workspace + // .update(cx, |workspace, cx| { + // workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) + // }) + // .detach(); + // cx.background_executor.run_until_parked(); + // let external_worktree_id = cx.read(|cx| { + // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + // assert_eq!( + // worktrees.len(), + // 2, + // "External file should get opened in a new worktree" + // ); + + // WorktreeId::from_usize( + // worktrees + // .into_iter() + // .find(|worktree| worktree.entity_id() != worktree_id.to_usize()) + // .expect("New worktree should have a different id") + // .id(), + // ) + // }); + // close_active_item(&workspace, cx).await; + + // let initial_history_items = + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // assert_eq!( + // initial_history_items, + // vec![FoundPath::new( + // ProjectPath { + // worktree_id: external_worktree_id, + // path: Arc::from(Path::new("")), + // }, + // Some(PathBuf::from("/external-src/test/third.rs")) + // )], + // "Should show external file with its full path in the history after it was open" + // ); + + // let updated_history_items = + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // assert_eq!( + // updated_history_items, + // vec![ + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/second.rs")), + // }, + // Some(PathBuf::from("/src/test/second.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id: external_worktree_id, + // path: Arc::from(Path::new("")), + // }, + // Some(PathBuf::from("/external-src/test/third.rs")) + // ), + // ], + // "Should keep external file with history updates", + // ); + // } + + #[gpui::test] + async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let cx = &mut cx; + + // generate some history to select from + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + let current_history = + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + for expected_selected_index in 0..current_history.len() { + cx.dispatch_action(Toggle); + let selected_index = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .read(cx) + .delegate + .selected_index() + }); + assert_eq!( + selected_index, expected_selected_index, + "Should select the next item in the history" + ); + } + + cx.dispatch_action(Toggle); + let selected_index = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .read(cx) + .delegate + .selected_index() + }); + assert_eq!( + selected_index, 0, + "Should wrap around the history and start all over" + ); + } + + // #[gpui::test] + // async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { + // let app_state = init_test(cx); + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/src", + // json!({ + // "test": { + // "first.rs": "// First Rust file", + // "second.rs": "// Second Rust file", + // "third.rs": "// Third Rust file", + // "fourth.rs": "// Fourth Rust file", + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // let worktree_id = cx.read(|cx| { + // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + // assert_eq!(worktrees.len(), 1,); + + // WorktreeId::from_usize(worktrees[0].entity_id()) + // }); + + // // generate some history to select from + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + // cx.dispatch_action(Toggle); + // let first_query = "f"; + // let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + // finder + // .update(cx, |finder, cx| { + // finder.delegate.update_matches(first_query.to_string(), cx) + // }) + // .await; + // finder.read_with(cx, |finder, _| { + // let delegate = &finder.delegate; + // assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); + // let history_match = delegate.matches.history.first().unwrap(); + // assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); + // assert_eq!(history_match.0, FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // )); + // assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); + // assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); + // }); + + // let second_query = "fsdasdsa"; + // let finder = workspace.update(cx, |workspace, cx| { + // workspace + // .current_modal::(cx) + // .unwrap() + // .read(cx) + // .picker + // }); + // finder + // .update(cx, |finder, cx| { + // finder.delegate.update_matches(second_query.to_string(), cx) + // }) + // .await; + // finder.update(cx, |finder, _| { + // let delegate = &finder.delegate; + // assert!( + // delegate.matches.history.is_empty(), + // "No history entries should match {second_query}" + // ); + // assert!( + // delegate.matches.search.is_empty(), + // "No search entries should match {second_query}" + // ); + // }); + + // let first_query_again = first_query; + + // let finder = workspace.update(cx, |workspace, cx| { + // workspace + // .current_modal::(cx) + // .unwrap() + // .read(cx) + // .picker + // }); + // finder + // .update(cx, |finder, cx| { + // finder + // .delegate + // .update_matches(first_query_again.to_string(), cx) + // }) + // .await; + // finder.read_with(cx, |finder, _| { + // let delegate = &finder.delegate; + // assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); + // let history_match = delegate.matches.history.first().unwrap(); + // assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); + // assert_eq!(history_match.0, FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // )); + // assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); + // assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); + // }); + // } + + // #[gpui::test] + // async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) { + // let app_state = init_test(cx); + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/src", + // json!({ + // "collab_ui": { + // "first.rs": "// First Rust file", + // "second.rs": "// Second Rust file", + // "third.rs": "// Third Rust file", + // "collab_ui.rs": "// Fourth Rust file", + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // // generate some history to select from + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + // cx.dispatch_action(Toggle); + // let query = "collab_ui"; + // let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + // finder + // .update(cx, |finder, cx| { + // finder.delegate.update_matches(query.to_string(), cx) + // }) + // .await; + // finder.read_with(cx, |finder, _| { + // let delegate = &finder.delegate; + // assert!( + // delegate.matches.history.is_empty(), + // "History items should not math query {query}, they should be matched by name only" + // ); + + // let search_entries = delegate + // .matches + // .search + // .iter() + // .map(|path_match| path_match.path.to_path_buf()) + // .collect::>(); + // assert_eq!( + // search_entries, + // vec![ + // PathBuf::from("collab_ui/collab_ui.rs"), + // PathBuf::from("collab_ui/third.rs"), + // PathBuf::from("collab_ui/first.rs"), + // PathBuf::from("collab_ui/second.rs"), + // ], + // "Despite all search results having the same directory name, the most matching one should be on top" + // ); + // }); + // } + + // #[gpui::test] + // async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) { + // let app_state = init_test(cx); + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/src", + // json!({ + // "test": { + // "first.rs": "// First Rust file", + // "nonexistent.rs": "// Second Rust file", + // "third.rs": "// Third Rust file", + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // // generate some history to select from + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + + // cx.dispatch_action(Toggle); + // let query = "rs"; + // let finder = cx.read(|cx| workspace.read(cx).current_modal::().unwrap()); + // finder + // .update(cx, |finder, cx| { + // finder.picker.update(cx, |picker, cx| { + // picker.delegate.update_matches(query.to_string(), cx) + // }) + // }) + // .await; + // finder.update(cx, |finder, _| { + // let history_entries = finder.delegate + // .matches + // .history + // .iter() + // .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) + // .collect::>(); + // assert_eq!( + // history_entries, + // vec![ + // PathBuf::from("test/first.rs"), + // PathBuf::from("test/third.rs"), + // ], + // "Should have all opened files in the history, except the ones that do not exist on disk" + // ); + // }); + // } + + async fn open_close_queried_buffer( + input: &str, + expected_matches: usize, + expected_editor_title: &str, + workspace: &View, + cx: &mut gpui::VisualTestContext<'_>, + ) -> Vec { + cx.dispatch_action(Toggle); + let picker = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); + picker + .update(cx, |finder, cx| { + finder.delegate.update_matches(input.to_string(), cx) + }) + .await; + let history_items = picker.update(cx, |finder, _| { + assert_eq!( + finder.delegate.matches.len(), + expected_matches, + "Unexpected number of matches found for query {input}" + ); + finder.delegate.history_items.clone() + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + cx.background_executor.run_until_parked(); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + cx.read(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + let active_editor_title = active_item + .to_any() + .downcast::() + .unwrap() + .read(cx) + .title(cx); + assert_eq!( + expected_editor_title, active_editor_title, + "Unexpected editor title for query {input}" + ); + }); + + close_active_item(workspace, cx).await; + + history_items + } + + async fn close_active_item(workspace: &View, cx: &mut VisualTestContext<'_>) { + let mut original_items = HashMap::new(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.entity_id(); + let pane = pane.read(cx); + let insertion_result = original_items.insert(pane_id, pane.items().count()); + assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); + } + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + active_pane + .update(cx, |pane, cx| { + pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) + .unwrap() + }) + .await + .unwrap(); + cx.background_executor.run_until_parked(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.entity_id(); + let pane = pane.read(cx); + match original_items.remove(&pane_id) { + Some(original_items) => { + assert_eq!( + pane.items().count(), + original_items.saturating_sub(1), + "Pane id {pane_id} should have item closed" + ); + } + None => panic!("Pane id {pane_id} not found in original items"), + } + } + }); + assert!( + original_items.len() <= 1, + "At most one panel should got closed" + ); + } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let state = AppState::test(cx); + theme::init(cx); + language::init(cx); + super::init(cx); + editor::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + state + }) + } + + fn test_path_like(test_str: &str) -> PathLikeWithPosition { + PathLikeWithPosition::parse_str(test_str, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: test_str.to_owned(), + file_query_end: if path_like_str == test_str { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .unwrap() + } + + fn dummy_found_path(project_path: ProjectPath) -> FoundPath { + FoundPath { + project: project_path, + absolute: None, + } + } + + fn build_find_picker( + project: Model, + cx: &mut TestAppContext, + ) -> ( + View>, + View, + VisualTestContext, + ) { + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + cx.dispatch_action(Toggle); + let picker = workspace.update(&mut cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); + (picker, workspace, cx) + } +} diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 44c31bbd69..850ddd6c9a 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,8 @@ use crate::{ - div, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, - Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, - ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext, - VisualContext, WindowContext, WindowHandle, WindowOptions, + div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, + BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, + Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TextStyle, + View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -83,8 +83,16 @@ impl TestAppContext { )); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); + let cx = AppContext::new(platform, asset_source, http_client); + let lock = cx.borrow_mut(); + lock.push_text_style(crate::TextStyleRefinement { + font_family: "Helvetica".into(), + ..Default::default() + }); + drop(lock); + Self { - app: AppContext::new(platform, asset_source, http_client), + app: cx, background_executor, foreground_executor, dispatcher: dispatcher.clone(), @@ -199,6 +207,15 @@ impl TestAppContext { } } + pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) + where + A: Action, + { + window + .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) + .unwrap() + } + pub fn dispatch_keystroke( &mut self, window: AnyWindowHandle, @@ -376,6 +393,13 @@ impl<'a> VisualTestContext<'a> { pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { Self { cx, window } } + + pub fn dispatch_action(&mut self, action: A) + where + A: Action, + { + self.cx.dispatch_action(self.window, action) + } } impl<'a> Context for VisualTestContext<'a> { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index efb586fe03..acbe851b4d 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -422,8 +422,11 @@ impl<'a> WindowContext<'a> { } pub fn dispatch_action(&mut self, action: Box) { + dbg!("BEFORE FOCUS"); if let Some(focus_handle) = self.focused() { + dbg!("BEFORE DEFER", focus_handle.id); self.defer(move |cx| { + dbg!("AFTER DEFER"); if let Some(node_id) = cx .window .current_frame diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index db012da38b..247c738161 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -38,10 +38,10 @@ use futures::{ use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, - StatefulInteractive, StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, - View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, - WindowOptions, + FocusHandle, FocusableKeyDispatch, GlobalPixels, KeyContext, Model, ModelContext, + ParentElement, Point, Render, Size, StatefulInteractive, StatefulInteractivity, + StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3409,10 +3409,6 @@ impl Workspace { // }); } - // todo!() - // #[cfg(any(test, feature = "test-support"))] - // pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { - // use node_runtime::FakeNodeRuntime; #[cfg(any(test, feature = "test-support"))] pub fn test_new(project: Model, cx: &mut ViewContext) -> Self { use gpui::Context; @@ -3432,7 +3428,10 @@ impl Workspace { initialize_workspace: |_, _, _, _| Task::ready(Ok(())), node_runtime: FakeNodeRuntime::new(), }); - Self::new(0, project, app_state, cx) + let workspace = Self::new(0, project, app_state, cx); + dbg!(&workspace.focus_handle); + workspace.focus_handle.focus(cx); + workspace } // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { @@ -3710,13 +3709,14 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA impl EventEmitter for Workspace {} impl Render for Workspace { - type Element = Div; + type Element = Div, FocusableKeyDispatch>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); context.add("Workspace"); self.add_workspace_actions_listeners(div()) + .track_focus(&self.focus_handle) .context(context) .relative() .size_full() From 008655b87930d832283411790cdc72834e19e997 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 14 Nov 2023 16:47:52 -0500 Subject: [PATCH 112/126] Set Pane Size --- crates/workspace2/src/dock.rs | 14 +++++++++++--- crates/workspace2/src/workspace2.rs | 22 +++++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 9a614bc92e..9fd4ace1c2 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,7 +1,8 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, FocusHandle, - ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView, WindowContext, + div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter, + FocusHandle, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView, + WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -429,7 +430,14 @@ impl Render for Dock { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { if let Some(entry) = self.visible_entry() { - div().size_full().child(entry.panel.to_any()) + let size = entry.panel.size(cx); + + div() + .map(|this| match self.position().axis() { + Axis::Horizontal => this.w(px(size)).h_full(), + Axis::Vertical => this.h(px(size)).w_full(), + }) + .child(entry.panel.to_any()) } else { div() } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 7137a273d5..272ffcf3cd 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -69,7 +69,8 @@ use std::{ }; use theme2::ActiveTheme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, TextTooltip}; +use ui::TextColor; +use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip}; use util::ResultExt; use uuid::Uuid; pub use workspace_settings::{AutosaveSetting, WorkspaceSettings}; @@ -3744,7 +3745,15 @@ impl Render for Workspace { .flex_row() .flex_1() .h_full() - .child(div().flex().flex_1().child(self.left_dock.clone())) + // Left Dock + .child( + div() + .flex() + .flex_none() + .overflow_hidden() + .child(self.left_dock.clone()), + ) + // Panes .child( div() .flex() @@ -3761,7 +3770,14 @@ impl Render for Workspace { )) .child(div().flex().flex_1().child(self.bottom_dock.clone())), ) - .child(div().flex().flex_1().child(self.right_dock.clone())), + // Right Dock + .child( + div() + .flex() + .flex_none() + .overflow_hidden() + .child(self.right_dock.clone()), + ), ), ) .child(self.status_bar.clone()) From 606ab74b9f2bd0db599a845eb6402d9c87397b46 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Nov 2023 13:18:19 -0800 Subject: [PATCH 113/126] Project panel: detect filename editor blur via an editor event --- crates/project_panel2/src/project_panel.rs | 48 ++++++++++++++-------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index e16ea364bd..1d44c4f116 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -197,23 +197,20 @@ impl ProjectPanel { editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => { this.autoscroll(cx); } + editor::Event::Blurred => { + if this + .edit_state + .as_ref() + .map_or(false, |state| state.processing_filename.is_none()) + { + this.edit_state = None; + this.update_visible_entries(None, cx); + } + } _ => {} }) .detach(); - // cx.observe_focus(&filename_editor, |this, _, is_focused, cx| { - // if !is_focused - // && this - // .edit_state - // .as_ref() - // .map_or(false, |state| state.processing_filename.is_none()) - // { - // this.edit_state = None; - // this.update_visible_entries(None, cx); - // } - // }) - // .detach(); - // cx.observe_global::(|_, cx| { // cx.notify(); // }) @@ -2360,7 +2357,11 @@ mod tests { cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v src <== selected", " > test"] + &[ + // + "v src <== selected", + " > test" + ] ); panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); panel.update(cx, |panel, cx| { @@ -2368,7 +2369,12 @@ mod tests { }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " > [EDITOR: ''] <== selected", " > test"] + &[ + // + "v src", + " > [EDITOR: ''] <== selected", + " > test" + ] ); panel.update(cx, |panel, cx| { panel @@ -2381,7 +2387,11 @@ mod tests { }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " > test"], + &[ + // + "v src", + " > test" + ], "File list should be unchanged after failed folder create confirmation" ); @@ -2390,7 +2400,11 @@ mod tests { cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " > test <== selected"] + &[ + // + "v src", + " > test <== selected" + ] ); panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); panel.update(cx, |panel, cx| { From 6b25841e2a2d22cba6669ccef4652d89dcfffe5e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 14:48:34 -0800 Subject: [PATCH 114/126] WIP --- crates/editor2/src/editor.rs | 4 ++-- crates/editor2/src/element.rs | 1 + crates/file_finder2/src/file_finder.rs | 3 ++- crates/gpui2/src/app/test_context.rs | 13 +++---------- crates/theme2/src/one_themes.rs | 1 + crates/theme2/src/settings.rs | 7 +++++++ crates/ui2/src/components/tooltip.rs | 6 ++++-- crates/ui2/src/to_extract/workspace.rs | 3 ++- crates/workspace2/src/workspace2.rs | 5 +++-- 9 files changed, 25 insertions(+), 18 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ebe78d95b3..84a80c9ebc 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9379,8 +9379,8 @@ impl Render for Editor { EditorMode::SingleLine => { TextStyle { color: cx.theme().colors().text, - font_family: "Zed Sans".into(), // todo!() - font_features: FontFeatures::default(), + font_family: settings.ui_font.family.clone(), // todo!() + font_features: settings.ui_font.features, font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index a68825fa77..4f7156a747 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1448,6 +1448,7 @@ impl EditorElement { let snapshot = editor.snapshot(cx); let style = self.style.clone(); + dbg!(&style.text.font()); let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); let font_size = style.text.font_size.to_pixels(cx.rem_size()); diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index c460cac252..2b78a24dea 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -39,11 +39,12 @@ impl FileFinder { Self::open(workspace, cx); return; }; + file_finder.update(cx, |file_finder, cx| { file_finder .picker .update(cx, |picker, cx| picker.cycle_selection(cx)) - }) + }); }); } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 850ddd6c9a..50447b2946 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,8 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, - Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TextStyle, - View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, + Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, + ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -83,16 +83,9 @@ impl TestAppContext { )); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); - let cx = AppContext::new(platform, asset_source, http_client); - let lock = cx.borrow_mut(); - lock.push_text_style(crate::TextStyleRefinement { - font_family: "Helvetica".into(), - ..Default::default() - }); - drop(lock); Self { - app: cx, + app: AppContext::new(platform, asset_source, http_client), background_executor, foreground_executor, dispatcher: dispatcher.clone(), diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index 6e32eace73..733cd6c40b 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -35,6 +35,7 @@ pub(crate) fn one_dark() -> Theme { id: "one_dark".to_string(), name: "One Dark".into(), appearance: Appearance::Dark, + styles: ThemeStyles { system: SystemColors::default(), colors: ThemeColors { diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 8a15b52641..5e3329ffa1 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -19,6 +19,7 @@ const MIN_LINE_HEIGHT: f32 = 1.0; #[derive(Clone)] pub struct ThemeSettings { pub ui_font_size: Pixels, + pub ui_font: Font, pub buffer_font: Font, pub buffer_font_size: Pixels, pub buffer_line_height: BufferLineHeight, @@ -120,6 +121,12 @@ impl settings::Settings for ThemeSettings { let mut this = Self { ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(), + ui_font: Font { + family: "Helvetica".into(), + features: Default::default(), + weight: Default::default(), + style: Default::default(), + }, buffer_font: Font { family: defaults.buffer_font_family.clone().unwrap().into(), features: defaults.buffer_font_features.clone().unwrap(), diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 58375b0b67..8463ed7ba4 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,5 +1,6 @@ use gpui::{Div, Render}; -use theme2::ActiveTheme; +use settings2::Settings; +use theme2::{ActiveTheme, ThemeSettings}; use crate::prelude::*; use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor}; @@ -34,9 +35,10 @@ impl Render for TextTooltip { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); v_stack() .elevation_2(cx) - .font("Zed Sans") + .font(ui_font) .text_ui_sm() .text_color(cx.theme().colors().text) .py_1() diff --git a/crates/ui2/src/to_extract/workspace.rs b/crates/ui2/src/to_extract/workspace.rs index d6de8a8288..0451a9d032 100644 --- a/crates/ui2/src/to_extract/workspace.rs +++ b/crates/ui2/src/to_extract/workspace.rs @@ -206,13 +206,14 @@ impl Render for Workspace { .child(self.editor_1.clone())], SplitDirection::Horizontal, ); + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); div() .relative() .size_full() .flex() .flex_col() - .font("Zed Sans") + .font(ui_font) .gap_0() .justify_start() .items_start() diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 247c738161..4786e7e35d 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -67,7 +67,7 @@ use std::{ sync::{atomic::AtomicUsize, Arc}, time::Duration, }; -use theme2::ActiveTheme; +use theme2::{ActiveTheme, ThemeSettings}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, TextTooltip}; use util::ResultExt; @@ -3714,6 +3714,7 @@ impl Render for Workspace { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); context.add("Workspace"); + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); self.add_workspace_actions_listeners(div()) .track_focus(&self.focus_handle) @@ -3722,7 +3723,7 @@ impl Render for Workspace { .size_full() .flex() .flex_col() - .font("Zed Sans") + .font(ui_font) .gap_0() .justify_start() .items_start() From 860959fe13d9c8d04cf6e858e7ace1d8d05c5556 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Nov 2023 14:56:50 -0800 Subject: [PATCH 115/126] Implement simulated prompts in TestPlatform --- crates/gpui2/src/app/test_context.rs | 19 +++--- crates/gpui2/src/platform/test/platform.rs | 78 +++++++++++++++++++--- crates/gpui2/src/platform/test/window.rs | 30 +++++---- crates/project_panel2/src/project_panel.rs | 8 +-- 4 files changed, 97 insertions(+), 38 deletions(-) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index c223f20532..919a7915e1 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -14,6 +14,7 @@ pub struct TestAppContext { pub background_executor: BackgroundExecutor, pub foreground_executor: ForegroundExecutor, pub dispatcher: TestDispatcher, + pub test_platform: Rc, } impl Context for TestAppContext { @@ -77,17 +78,15 @@ impl TestAppContext { let arc_dispatcher = Arc::new(dispatcher.clone()); let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); let foreground_executor = ForegroundExecutor::new(arc_dispatcher); - let platform = Rc::new(TestPlatform::new( - background_executor.clone(), - foreground_executor.clone(), - )); + let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone()); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); Self { - app: AppContext::new(platform, asset_source, http_client), + app: AppContext::new(platform.clone(), asset_source, http_client), background_executor, foreground_executor, dispatcher: dispatcher.clone(), + test_platform: platform, } } @@ -154,17 +153,17 @@ impl TestAppContext { pub fn simulate_new_path_selection( &self, - _select_path: impl FnOnce(&std::path::Path) -> Option, + select_path: impl FnOnce(&std::path::Path) -> Option, ) { - // + self.test_platform.simulate_new_path_selection(select_path); } - pub fn simulate_prompt_answer(&self, _button_ix: usize) { - // + pub fn simulate_prompt_answer(&self, button_ix: usize) { + self.test_platform.simulate_prompt_answer(button_ix); } pub fn has_pending_prompt(&self) -> bool { - false + self.test_platform.has_pending_prompt() } pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 4afcc4fc1a..3e151ab810 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -3,8 +3,15 @@ use crate::{ PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, }; use anyhow::{anyhow, Result}; +use collections::VecDeque; +use futures::channel::oneshot; use parking_lot::Mutex; -use std::{rc::Rc, sync::Arc}; +use std::{ + cell::RefCell, + path::PathBuf, + rc::{Rc, Weak}, + sync::Arc, +}; pub struct TestPlatform { background_executor: BackgroundExecutor, @@ -13,18 +20,60 @@ pub struct TestPlatform { active_window: Arc>>, active_display: Rc, active_cursor: Mutex, + pub(crate) prompts: RefCell, + weak: Weak, +} + +#[derive(Default)] +pub(crate) struct TestPrompts { + multiple_choice: VecDeque>, + new_path: VecDeque<(PathBuf, oneshot::Sender>)>, } impl TestPlatform { - pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Self { - TestPlatform { + pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc { + Rc::new_cyclic(|weak| TestPlatform { background_executor: executor, foreground_executor, - + prompts: Default::default(), active_cursor: Default::default(), active_display: Rc::new(TestDisplay::new()), active_window: Default::default(), - } + weak: weak.clone(), + }) + } + + pub(crate) fn simulate_new_path_selection( + &self, + select_path: impl FnOnce(&std::path::Path) -> Option, + ) { + let (path, tx) = self + .prompts + .borrow_mut() + .new_path + .pop_front() + .expect("no pending new path prompt"); + tx.send(select_path(&path)).ok(); + } + + pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) { + let tx = self + .prompts + .borrow_mut() + .multiple_choice + .pop_front() + .expect("no pending multiple choice prompt"); + tx.send(response_ix).ok(); + } + + pub(crate) fn has_pending_prompt(&self) -> bool { + !self.prompts.borrow().multiple_choice.is_empty() + } + + pub(crate) fn prompt(&self) -> oneshot::Receiver { + let (tx, rx) = oneshot::channel(); + self.prompts.borrow_mut().multiple_choice.push_back(tx); + rx } } @@ -88,7 +137,11 @@ impl Platform for TestPlatform { options: WindowOptions, ) -> Box { *self.active_window.lock() = Some(handle); - Box::new(TestWindow::new(options, self.active_display.clone())) + Box::new(TestWindow::new( + options, + self.weak.clone(), + self.active_display.clone(), + )) } fn set_display_link_output_callback( @@ -118,15 +171,20 @@ impl Platform for TestPlatform { fn prompt_for_paths( &self, _options: crate::PathPromptOptions, - ) -> futures::channel::oneshot::Receiver>> { + ) -> oneshot::Receiver>> { unimplemented!() } fn prompt_for_new_path( &self, - _directory: &std::path::Path, - ) -> futures::channel::oneshot::Receiver> { - unimplemented!() + directory: &std::path::Path, + ) -> oneshot::Receiver> { + let (tx, rx) = oneshot::channel(); + self.prompts + .borrow_mut() + .new_path + .push_back((directory.to_path_buf(), tx)); + rx } fn reveal_path(&self, _path: &std::path::Path) { diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index cf9143162e..adb15c4266 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -1,15 +1,13 @@ -use std::{ - rc::Rc, - sync::{self, Arc}, -}; - -use collections::HashMap; -use parking_lot::Mutex; - use crate::{ px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, - PlatformInputHandler, PlatformWindow, Point, Scene, Size, TileId, WindowAppearance, - WindowBounds, WindowOptions, + PlatformInputHandler, PlatformWindow, Point, Scene, Size, TestPlatform, TileId, + WindowAppearance, WindowBounds, WindowOptions, +}; +use collections::HashMap; +use parking_lot::Mutex; +use std::{ + rc::{Rc, Weak}, + sync::{self, Arc}, }; #[derive(Default)] @@ -25,16 +23,22 @@ pub struct TestWindow { current_scene: Mutex>, display: Rc, input_handler: Option>, - handlers: Mutex, + platform: Weak, sprite_atlas: Arc, } + impl TestWindow { - pub fn new(options: WindowOptions, display: Rc) -> Self { + pub fn new( + options: WindowOptions, + platform: Weak, + display: Rc, + ) -> Self { Self { bounds: options.bounds, current_scene: Default::default(), display, + platform, input_handler: None, sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), @@ -89,7 +93,7 @@ impl PlatformWindow for TestWindow { _msg: &str, _answers: &[&str], ) -> futures::channel::oneshot::Receiver { - todo!() + self.platform.upgrade().expect("platform dropped").prompt() } fn activate(&self) { diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 1d44c4f116..ac58313351 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -2033,7 +2033,7 @@ mod tests { ); } - #[gpui::test(iterations = 30)] + #[gpui::test(iterations = 10)] async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -2653,17 +2653,15 @@ mod tests { .unwrap(); // "Save as"" the buffer, creating a new backing file for it - workspace + let save_task = workspace .update(cx, |workspace, cx| { workspace.save_active_item(workspace::SaveIntent::Save, cx) }) - .unwrap() - .await .unwrap(); cx.executor().run_until_parked(); cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); - cx.executor().run_until_parked(); + save_task.await.unwrap(); // Rename the file select_path(&panel, "root/new", cx); From bef4df5df98d6e715589a20c4cd92f3c67f70002 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 16:05:28 -0700 Subject: [PATCH 116/126] Return an id from `Element::element_id` instead of calling `cx.with_element_id` Co-Authored-By: Julia --- crates/editor2/src/editor.rs | 19 +- crates/editor2/src/element.rs | 394 +++++++++++++++++----------------- 2 files changed, 209 insertions(+), 204 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 0f349ceda7..5cfd349dc9 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9409,14 +9409,17 @@ impl Render for Editor { EditorMode::Full => cx.theme().colors().editor_background, }; - EditorElement::new(EditorStyle { - background, - local_player: cx.theme().players().local(), - text: text_style, - scrollbar_width: px(12.), - syntax: cx.theme().syntax().clone(), - diagnostic_style: cx.theme().diagnostic_style(), - }) + EditorElement::new( + cx.view(), + EditorStyle { + background, + local_player: cx.theme().players().local(), + text: text_style, + scrollbar_width: px(12.), + syntax: cx.theme().syntax().clone(), + diagnostic_style: cx.theme().diagnostic_style(), + }, + ) } } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index d06f73c92b..844e6a645f 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -20,9 +20,9 @@ use collections::{BTreeMap, HashMap}; use gpui::{ point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, - ElementInputHandler, Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, + ElementInputHandler, Entity, EntityId, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, ScrollWheelEvent, Size, Style, Styled, TextRun, - TextStyle, ViewContext, WindowContext, + TextStyle, View, ViewContext, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -111,12 +111,16 @@ impl SelectionLayout { } pub struct EditorElement { + editor_id: EntityId, style: EditorStyle, } impl EditorElement { - pub fn new(style: EditorStyle) -> Self { - Self { style } + pub fn new(editor: &View, style: EditorStyle) -> Self { + Self { + editor_id: editor.entity_id(), + style, + } } fn mouse_down( @@ -2404,7 +2408,7 @@ impl Element for EditorElement { type ElementState = (); fn element_id(&self) -> Option { - None // todo! can we change the element trait to return an id here from the view context? + Some(self.editor_id.into()) } fn initialize( @@ -2451,201 +2455,199 @@ impl Element for EditorElement { }; let dispatch_context = editor.dispatch_context(cx); - cx.with_element_id(Some(cx.view().entity_id()), |cx| { - cx.with_key_dispatch( - dispatch_context, - Some(editor.focus_handle.clone()), - |_, cx| { - register_action(cx, Editor::move_left); - register_action(cx, Editor::move_right); - register_action(cx, Editor::move_down); - register_action(cx, Editor::move_up); - // on_action(cx, Editor::new_file); todo!() - // on_action(cx, Editor::new_file_in_direction); todo!() - register_action(cx, Editor::cancel); - register_action(cx, Editor::newline); - register_action(cx, Editor::newline_above); - register_action(cx, Editor::newline_below); - register_action(cx, Editor::backspace); - register_action(cx, Editor::delete); - register_action(cx, Editor::tab); - register_action(cx, Editor::tab_prev); - register_action(cx, Editor::indent); - register_action(cx, Editor::outdent); - register_action(cx, Editor::delete_line); - register_action(cx, Editor::join_lines); - register_action(cx, Editor::sort_lines_case_sensitive); - register_action(cx, Editor::sort_lines_case_insensitive); - register_action(cx, Editor::reverse_lines); - register_action(cx, Editor::shuffle_lines); - register_action(cx, Editor::convert_to_upper_case); - register_action(cx, Editor::convert_to_lower_case); - register_action(cx, Editor::convert_to_title_case); - register_action(cx, Editor::convert_to_snake_case); - register_action(cx, Editor::convert_to_kebab_case); - register_action(cx, Editor::convert_to_upper_camel_case); - register_action(cx, Editor::convert_to_lower_camel_case); - register_action(cx, Editor::delete_to_previous_word_start); - register_action(cx, Editor::delete_to_previous_subword_start); - register_action(cx, Editor::delete_to_next_word_end); - register_action(cx, Editor::delete_to_next_subword_end); - register_action(cx, Editor::delete_to_beginning_of_line); - register_action(cx, Editor::delete_to_end_of_line); - register_action(cx, Editor::cut_to_end_of_line); - register_action(cx, Editor::duplicate_line); - register_action(cx, Editor::move_line_up); - register_action(cx, Editor::move_line_down); - register_action(cx, Editor::transpose); - register_action(cx, Editor::cut); - register_action(cx, Editor::copy); - register_action(cx, Editor::paste); - register_action(cx, Editor::undo); - register_action(cx, Editor::redo); - register_action(cx, Editor::move_page_up); - register_action(cx, Editor::move_page_down); - register_action(cx, Editor::next_screen); - register_action(cx, Editor::scroll_cursor_top); - register_action(cx, Editor::scroll_cursor_center); - register_action(cx, Editor::scroll_cursor_bottom); - register_action(cx, |editor, _: &LineDown, cx| { - editor.scroll_screen(&ScrollAmount::Line(1.), cx) - }); - register_action(cx, |editor, _: &LineUp, cx| { - editor.scroll_screen(&ScrollAmount::Line(-1.), cx) - }); - register_action(cx, |editor, _: &HalfPageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(0.5), cx) - }); - register_action(cx, |editor, _: &HalfPageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) - }); - register_action(cx, |editor, _: &PageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(1.), cx) - }); - register_action(cx, |editor, _: &PageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-1.), cx) - }); - register_action(cx, Editor::move_to_previous_word_start); - register_action(cx, Editor::move_to_previous_subword_start); - register_action(cx, Editor::move_to_next_word_end); - register_action(cx, Editor::move_to_next_subword_end); - register_action(cx, Editor::move_to_beginning_of_line); - register_action(cx, Editor::move_to_end_of_line); - register_action(cx, Editor::move_to_start_of_paragraph); - register_action(cx, Editor::move_to_end_of_paragraph); - register_action(cx, Editor::move_to_beginning); - register_action(cx, Editor::move_to_end); - register_action(cx, Editor::select_up); - register_action(cx, Editor::select_down); - register_action(cx, Editor::select_left); - register_action(cx, Editor::select_right); - register_action(cx, Editor::select_to_previous_word_start); - register_action(cx, Editor::select_to_previous_subword_start); - register_action(cx, Editor::select_to_next_word_end); - register_action(cx, Editor::select_to_next_subword_end); - register_action(cx, Editor::select_to_beginning_of_line); - register_action(cx, Editor::select_to_end_of_line); - register_action(cx, Editor::select_to_start_of_paragraph); - register_action(cx, Editor::select_to_end_of_paragraph); - register_action(cx, Editor::select_to_beginning); - register_action(cx, Editor::select_to_end); - register_action(cx, Editor::select_all); - register_action(cx, |editor, action, cx| { - editor.select_all_matches(action, cx).log_err(); - }); - register_action(cx, Editor::select_line); - register_action(cx, Editor::split_selection_into_lines); - register_action(cx, Editor::add_selection_above); - register_action(cx, Editor::add_selection_below); - register_action(cx, |editor, action, cx| { - editor.select_next(action, cx).log_err(); - }); - register_action(cx, |editor, action, cx| { - editor.select_previous(action, cx).log_err(); - }); - register_action(cx, Editor::toggle_comments); - register_action(cx, Editor::select_larger_syntax_node); - register_action(cx, Editor::select_smaller_syntax_node); - register_action(cx, Editor::move_to_enclosing_bracket); - register_action(cx, Editor::undo_selection); - register_action(cx, Editor::redo_selection); - register_action(cx, Editor::go_to_diagnostic); - register_action(cx, Editor::go_to_prev_diagnostic); - register_action(cx, Editor::go_to_hunk); - register_action(cx, Editor::go_to_prev_hunk); - register_action(cx, Editor::go_to_definition); - register_action(cx, Editor::go_to_definition_split); - register_action(cx, Editor::go_to_type_definition); - register_action(cx, Editor::go_to_type_definition_split); - register_action(cx, Editor::fold); - register_action(cx, Editor::fold_at); - register_action(cx, Editor::unfold_lines); - register_action(cx, Editor::unfold_at); - register_action(cx, Editor::fold_selected_ranges); - register_action(cx, Editor::show_completions); - register_action(cx, Editor::toggle_code_actions); - // on_action(cx, Editor::open_excerpts); todo!() - register_action(cx, Editor::toggle_soft_wrap); - register_action(cx, Editor::toggle_inlay_hints); - register_action(cx, Editor::reveal_in_finder); - register_action(cx, Editor::copy_path); - register_action(cx, Editor::copy_relative_path); - register_action(cx, Editor::copy_highlight_json); - register_action(cx, |editor, action, cx| { - editor - .format(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, Editor::restart_language_server); - register_action(cx, Editor::show_character_palette); - // on_action(cx, Editor::confirm_completion); todo!() - register_action(cx, |editor, action, cx| { - editor - .confirm_code_action(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - // on_action(cx, Editor::rename); todo!() - // on_action(cx, Editor::confirm_rename); todo!() - register_action(cx, |editor, action, cx| { - editor - .find_all_references(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, Editor::next_copilot_suggestion); - register_action(cx, Editor::previous_copilot_suggestion); - register_action(cx, Editor::copilot_suggest); - register_action(cx, Editor::context_menu_first); - register_action(cx, Editor::context_menu_prev); - register_action(cx, Editor::context_menu_next); - register_action(cx, Editor::context_menu_last); + cx.with_key_dispatch( + dispatch_context, + Some(editor.focus_handle.clone()), + |_, cx| { + register_action(cx, Editor::move_left); + register_action(cx, Editor::move_right); + register_action(cx, Editor::move_down); + register_action(cx, Editor::move_up); + // on_action(cx, Editor::new_file); todo!() + // on_action(cx, Editor::new_file_in_direction); todo!() + register_action(cx, Editor::cancel); + register_action(cx, Editor::newline); + register_action(cx, Editor::newline_above); + register_action(cx, Editor::newline_below); + register_action(cx, Editor::backspace); + register_action(cx, Editor::delete); + register_action(cx, Editor::tab); + register_action(cx, Editor::tab_prev); + register_action(cx, Editor::indent); + register_action(cx, Editor::outdent); + register_action(cx, Editor::delete_line); + register_action(cx, Editor::join_lines); + register_action(cx, Editor::sort_lines_case_sensitive); + register_action(cx, Editor::sort_lines_case_insensitive); + register_action(cx, Editor::reverse_lines); + register_action(cx, Editor::shuffle_lines); + register_action(cx, Editor::convert_to_upper_case); + register_action(cx, Editor::convert_to_lower_case); + register_action(cx, Editor::convert_to_title_case); + register_action(cx, Editor::convert_to_snake_case); + register_action(cx, Editor::convert_to_kebab_case); + register_action(cx, Editor::convert_to_upper_camel_case); + register_action(cx, Editor::convert_to_lower_camel_case); + register_action(cx, Editor::delete_to_previous_word_start); + register_action(cx, Editor::delete_to_previous_subword_start); + register_action(cx, Editor::delete_to_next_word_end); + register_action(cx, Editor::delete_to_next_subword_end); + register_action(cx, Editor::delete_to_beginning_of_line); + register_action(cx, Editor::delete_to_end_of_line); + register_action(cx, Editor::cut_to_end_of_line); + register_action(cx, Editor::duplicate_line); + register_action(cx, Editor::move_line_up); + register_action(cx, Editor::move_line_down); + register_action(cx, Editor::transpose); + register_action(cx, Editor::cut); + register_action(cx, Editor::copy); + register_action(cx, Editor::paste); + register_action(cx, Editor::undo); + register_action(cx, Editor::redo); + register_action(cx, Editor::move_page_up); + register_action(cx, Editor::move_page_down); + register_action(cx, Editor::next_screen); + register_action(cx, Editor::scroll_cursor_top); + register_action(cx, Editor::scroll_cursor_center); + register_action(cx, Editor::scroll_cursor_bottom); + register_action(cx, |editor, _: &LineDown, cx| { + editor.scroll_screen(&ScrollAmount::Line(1.), cx) + }); + register_action(cx, |editor, _: &LineUp, cx| { + editor.scroll_screen(&ScrollAmount::Line(-1.), cx) + }); + register_action(cx, |editor, _: &HalfPageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(0.5), cx) + }); + register_action(cx, |editor, _: &HalfPageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) + }); + register_action(cx, |editor, _: &PageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.), cx) + }); + register_action(cx, |editor, _: &PageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-1.), cx) + }); + register_action(cx, Editor::move_to_previous_word_start); + register_action(cx, Editor::move_to_previous_subword_start); + register_action(cx, Editor::move_to_next_word_end); + register_action(cx, Editor::move_to_next_subword_end); + register_action(cx, Editor::move_to_beginning_of_line); + register_action(cx, Editor::move_to_end_of_line); + register_action(cx, Editor::move_to_start_of_paragraph); + register_action(cx, Editor::move_to_end_of_paragraph); + register_action(cx, Editor::move_to_beginning); + register_action(cx, Editor::move_to_end); + register_action(cx, Editor::select_up); + register_action(cx, Editor::select_down); + register_action(cx, Editor::select_left); + register_action(cx, Editor::select_right); + register_action(cx, Editor::select_to_previous_word_start); + register_action(cx, Editor::select_to_previous_subword_start); + register_action(cx, Editor::select_to_next_word_end); + register_action(cx, Editor::select_to_next_subword_end); + register_action(cx, Editor::select_to_beginning_of_line); + register_action(cx, Editor::select_to_end_of_line); + register_action(cx, Editor::select_to_start_of_paragraph); + register_action(cx, Editor::select_to_end_of_paragraph); + register_action(cx, Editor::select_to_beginning); + register_action(cx, Editor::select_to_end); + register_action(cx, Editor::select_all); + register_action(cx, |editor, action, cx| { + editor.select_all_matches(action, cx).log_err(); + }); + register_action(cx, Editor::select_line); + register_action(cx, Editor::split_selection_into_lines); + register_action(cx, Editor::add_selection_above); + register_action(cx, Editor::add_selection_below); + register_action(cx, |editor, action, cx| { + editor.select_next(action, cx).log_err(); + }); + register_action(cx, |editor, action, cx| { + editor.select_previous(action, cx).log_err(); + }); + register_action(cx, Editor::toggle_comments); + register_action(cx, Editor::select_larger_syntax_node); + register_action(cx, Editor::select_smaller_syntax_node); + register_action(cx, Editor::move_to_enclosing_bracket); + register_action(cx, Editor::undo_selection); + register_action(cx, Editor::redo_selection); + register_action(cx, Editor::go_to_diagnostic); + register_action(cx, Editor::go_to_prev_diagnostic); + register_action(cx, Editor::go_to_hunk); + register_action(cx, Editor::go_to_prev_hunk); + register_action(cx, Editor::go_to_definition); + register_action(cx, Editor::go_to_definition_split); + register_action(cx, Editor::go_to_type_definition); + register_action(cx, Editor::go_to_type_definition_split); + register_action(cx, Editor::fold); + register_action(cx, Editor::fold_at); + register_action(cx, Editor::unfold_lines); + register_action(cx, Editor::unfold_at); + register_action(cx, Editor::fold_selected_ranges); + register_action(cx, Editor::show_completions); + register_action(cx, Editor::toggle_code_actions); + // on_action(cx, Editor::open_excerpts); todo!() + register_action(cx, Editor::toggle_soft_wrap); + register_action(cx, Editor::toggle_inlay_hints); + register_action(cx, Editor::reveal_in_finder); + register_action(cx, Editor::copy_path); + register_action(cx, Editor::copy_relative_path); + register_action(cx, Editor::copy_highlight_json); + register_action(cx, |editor, action, cx| { + editor + .format(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, Editor::restart_language_server); + register_action(cx, Editor::show_character_palette); + // on_action(cx, Editor::confirm_completion); todo!() + register_action(cx, |editor, action, cx| { + editor + .confirm_code_action(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + // on_action(cx, Editor::rename); todo!() + // on_action(cx, Editor::confirm_rename); todo!() + register_action(cx, |editor, action, cx| { + editor + .find_all_references(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, Editor::next_copilot_suggestion); + register_action(cx, Editor::previous_copilot_suggestion); + register_action(cx, Editor::copilot_suggest); + register_action(cx, Editor::context_menu_first); + register_action(cx, Editor::context_menu_prev); + register_action(cx, Editor::context_menu_next); + register_action(cx, Editor::context_menu_last); - // We call with_z_index to establish a new stacking context. - cx.with_z_index(0, |cx| { - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - self.paint_mouse_listeners( - bounds, - gutter_bounds, - text_bounds, - &layout.position_map, - cx, - ); - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &mut layout, editor, cx); - } - self.paint_text(text_bounds, &mut layout, editor, cx); + // We call with_z_index to establish a new stacking context. + cx.with_z_index(0, |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + self.paint_mouse_listeners( + bounds, + gutter_bounds, + text_bounds, + &layout.position_map, + cx, + ); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); + if layout.gutter_size.width > Pixels::ZERO { + self.paint_gutter(gutter_bounds, &mut layout, editor, cx); + } + self.paint_text(text_bounds, &mut layout, editor, cx); - if !layout.blocks.is_empty() { - self.paint_blocks(bounds, &mut layout, editor, cx); - } + if !layout.blocks.is_empty() { + self.paint_blocks(bounds, &mut layout, editor, cx); + } - let input_handler = ElementInputHandler::new(bounds, cx); - cx.handle_input(&editor.focus_handle, input_handler); - }); + let input_handler = ElementInputHandler::new(bounds, cx); + cx.handle_input(&editor.focus_handle, input_handler); }); - }, - ) - }); + }); + }, + ) } } From 1109cd11c81e21de4b9e6762b843a168d822cec5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 16:16:02 -0700 Subject: [PATCH 117/126] Abandon ship --- crates/editor2/src/element.rs | 1 - crates/file_finder2/src/file_finder.rs | 2468 ++++++++++---------- crates/gpui2/src/elements/uniform_list.rs | 2 +- crates/project_panel2/src/project_panel.rs | 3 +- crates/workspace2/src/workspace2.rs | 22 +- 5 files changed, 1247 insertions(+), 1249 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 4f7156a747..4f3bda3752 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1449,7 +1449,6 @@ impl EditorElement { let snapshot = editor.snapshot(cx); let style = self.style.clone(); - dbg!(&style.text.font()); let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); let font_size = style.text.font_size.to_pixels(cx.rem_size()); let line_height = style.text.line_height_in_pixels(cx.rem_size()); diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 2b78a24dea..72638f603f 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -593,6 +593,7 @@ impl PickerDelegate for FileFinderDelegate { } fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { + dbg!("CONFIRMING???"); if let Some(m) = self.matches.get(self.selected_index()) { if let Some(workspace) = self.workspace.upgrade() { let open_task = workspace.update(cx, move |workspace, cx| { @@ -690,6 +691,7 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } + dbg!("DISMISSING"); finder .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed)) .ok()?; @@ -739,1236 +741,1236 @@ impl PickerDelegate for FileFinderDelegate { } } -#[cfg(test)] -mod tests { - use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; - - use super::*; - use editor::Editor; - use gpui::{Entity, TestAppContext, VisualTestContext}; - use menu::{Confirm, SelectNext}; - use serde_json::json; - use workspace::{AppState, Workspace}; - - #[ctor::ctor] - fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } - } - - #[gpui::test] - async fn test_matching_paths(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "a": { - "banana": "", - "bandana": "", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - - let (picker, workspace, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; - - picker - .update(cx, |picker, cx| { - picker.delegate.update_matches("bna".to_string(), cx) - }) - .await; - - picker.update(cx, |picker, _| { - assert_eq!(picker.delegate.matches.len(), 2); - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(SelectNext); - cx.dispatch_action(Confirm); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - cx.read(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - assert_eq!( - active_item - .to_any() - .downcast::() - .unwrap() - .read(cx) - .title(cx), - "bandana" - ); - }); - } - - #[gpui::test] - async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - let first_file_name = "first.rs"; - let first_file_contents = "// First Rust file"; - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - first_file_name: first_file_contents, - "second.rs": "// Second Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - - let (picker, workspace, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; - - let file_query = &first_file_name[..3]; - let file_row = 1; - let file_column = 3; - assert!(file_column <= first_file_contents.len()); - let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); - picker - .update(cx, |finder, cx| { - finder - .delegate - .update_matches(query_inside_file.to_string(), cx) - }) - .await; - picker.update(cx, |finder, _| { - let finder = &finder.delegate; - assert_eq!(finder.matches.len(), 1); - let latest_search_query = finder - .latest_search_query - .as_ref() - .expect("Finder should have a query after the update_matches call"); - assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); - assert_eq!( - latest_search_query.path_like.file_query_end, - Some(file_query.len()) - ); - assert_eq!(latest_search_query.row, Some(file_row)); - assert_eq!(latest_search_query.column, Some(file_column as u32)); - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(SelectNext); - cx.dispatch_action(Confirm); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - let editor = cx.update(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - active_item.downcast::().unwrap() - }); - cx.executor().advance_clock(Duration::from_secs(2)); - - editor.update(cx, |editor, cx| { - let all_selections = editor.selections.all_adjusted(cx); - assert_eq!( - all_selections.len(), - 1, - "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" - ); - let caret_selection = all_selections.into_iter().next().unwrap(); - assert_eq!(caret_selection.start, caret_selection.end, - "Caret selection should have its start and end at the same position"); - assert_eq!(file_row, caret_selection.start.row + 1, - "Query inside file should get caret with the same focus row"); - assert_eq!(file_column, caret_selection.start.column as usize + 1, - "Query inside file should get caret with the same focus column"); - }); - } - - #[gpui::test] - async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - let first_file_name = "first.rs"; - let first_file_contents = "// First Rust file"; - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - first_file_name: first_file_contents, - "second.rs": "// Second Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - - let (picker, workspace, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; - - let file_query = &first_file_name[..3]; - let file_row = 200; - let file_column = 300; - assert!(file_column > first_file_contents.len()); - let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); - picker - .update(cx, |picker, cx| { - picker - .delegate - .update_matches(query_outside_file.to_string(), cx) - }) - .await; - picker.update(cx, |finder, _| { - let delegate = &finder.delegate; - assert_eq!(delegate.matches.len(), 1); - let latest_search_query = delegate - .latest_search_query - .as_ref() - .expect("Finder should have a query after the update_matches call"); - assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); - assert_eq!( - latest_search_query.path_like.file_query_end, - Some(file_query.len()) - ); - assert_eq!(latest_search_query.row, Some(file_row)); - assert_eq!(latest_search_query.column, Some(file_column as u32)); - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(SelectNext); - cx.dispatch_action(Confirm); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - let editor = cx.update(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - active_item.downcast::().unwrap() - }); - cx.executor().advance_clock(Duration::from_secs(2)); - - editor.update(cx, |editor, cx| { - let all_selections = editor.selections.all_adjusted(cx); - assert_eq!( - all_selections.len(), - 1, - "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" - ); - let caret_selection = all_selections.into_iter().next().unwrap(); - assert_eq!(caret_selection.start, caret_selection.end, - "Caret selection should have its start and end at the same position"); - assert_eq!(0, caret_selection.start.row, - "Excessive rows (as in query outside file borders) should get trimmed to last file row"); - assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, - "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); - }); - } - - #[gpui::test] - async fn test_matching_cancellation(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/dir", - json!({ - "hello": "", - "goodbye": "", - "halogen-light": "", - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; - - let (picker, _, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; - - let query = test_path_like("hi"); - picker - .update(cx, |picker, cx| { - picker.delegate.spawn_search(query.clone(), cx) - }) - .await; - - picker.update(cx, |picker, _cx| { - assert_eq!(picker.delegate.matches.len(), 5) - }); - - picker.update(cx, |picker, cx| { - let delegate = &mut picker.delegate; - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - let matches = delegate.matches.search.clone(); - - // Simulate a search being cancelled after the time limit, - // returning only a subset of the matches that would have been found. - drop(delegate.spawn_search(query.clone(), cx)); - delegate.set_search_matches( - delegate.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[1].clone(), matches[3].clone()], - cx, - ); - - // Simulate another cancellation. - drop(delegate.spawn_search(query.clone(), cx)); - delegate.set_search_matches( - delegate.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], - cx, - ); - - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); - }); - } - - #[gpui::test] - async fn test_ignored_files(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/ancestor", - json!({ - ".gitignore": "ignored-root", - "ignored-root": { - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - }, - "tracked-root": { - ".gitignore": "height", - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - }, - }), - ) - .await; - - let project = Project::test( - app_state.fs.clone(), - [ - "/ancestor/tracked-root".as_ref(), - "/ancestor/ignored-root".as_ref(), - ], - cx, - ) - .await; - - let (picker, _, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; - - picker - .update(cx, |picker, cx| { - picker.delegate.spawn_search(test_path_like("hi"), cx) - }) - .await; - picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7)); - } - - // #[gpui::test] - // async fn test_single_file_worktrees(cx: &mut TestAppContext) { - // let app_state = init_test(cx); - // app_state - // .fs - // .as_fake() - // .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) - // .await; - - // let project = Project::test( - // app_state.fs.clone(), - // ["/root/the-parent-dir/the-file".as_ref()], - // cx, - // ) - // .await; - - // let (picker, _, mut cx) = build_find_picker(project, cx); - // let cx = &mut cx; - - // // Even though there is only one worktree, that worktree's filename - // // is included in the matching, because the worktree is a single file. - // picker - // .update(cx, |picker, cx| { - // picker.delegate.spawn_search(test_path_like("thf"), cx) - // }) - // .await; - // cx.read(|cx| { - // let picker = picker.read(cx); - // let delegate = &picker.delegate; - // assert!( - // delegate.matches.history.is_empty(), - // "Search matches expected" - // ); - // let matches = delegate.matches.search.clone(); - // assert_eq!(matches.len(), 1); - - // let (file_name, file_name_positions, full_path, full_path_positions) = - // delegate.labels_for_path_match(&matches[0]); - // assert_eq!(file_name, "the-file"); - // assert_eq!(file_name_positions, &[0, 1, 4]); - // assert_eq!(full_path, "the-file"); - // assert_eq!(full_path_positions, &[0, 1, 4]); - // }); - - // // Since the worktree root is a file, searching for its name followed by a slash does - // // not match anything. - // picker - // .update(cx, |f, cx| { - // f.delegate.spawn_search(test_path_like("thf/"), cx) - // }) - // .await; - // picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0)); - // } - - // #[gpui::test] - // async fn test_path_distance_ordering(cx: &mut TestAppContext) { - // let app_state = init_test(cx); - // app_state - // .fs - // .as_fake() - // .insert_tree( - // "/root", - // json!({ - // "dir1": { "a.txt": "" }, - // "dir2": { - // "a.txt": "", - // "b.txt": "" - // } - // }), - // ) - // .await; - - // let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut cx; - - // let worktree_id = cx.read(|cx| { - // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - // assert_eq!(worktrees.len(), 1); - // WorktreeId::from_usize(worktrees[0].id()) - // }); - - // // When workspace has an active item, sort items which are closer to that item - // // first when they have the same name. In this case, b.txt is closer to dir2's a.txt - // // so that one should be sorted earlier - // let b_path = Some(dummy_found_path(ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("/root/dir2/b.txt")), - // })); - // cx.dispatch_action(Toggle); - - // let finder = cx - // .add_window(|cx| { - // Picker::new( - // FileFinderDelegate::new( - // workspace.downgrade(), - // workspace.read(cx).project().clone(), - // b_path, - // Vec::new(), - // cx, - // ), - // cx, - // ) - // }) - // .root(cx); - - // finder - // .update(cx, |f, cx| { - // f.delegate.spawn_search(test_path_like("a.txt"), cx) - // }) - // .await; - - // finder.read_with(cx, |f, _| { - // let delegate = &f.delegate; - // assert!( - // delegate.matches.history.is_empty(), - // "Search matches expected" - // ); - // let matches = delegate.matches.search.clone(); - // assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); - // assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); - // }); - // } - - // #[gpui::test] - // async fn test_search_worktree_without_files(cx: &mut TestAppContext) { - // let app_state = init_test(cx); - // app_state - // .fs - // .as_fake() - // .insert_tree( - // "/root", - // json!({ - // "dir1": {}, - // "dir2": { - // "dir3": {} - // } - // }), - // ) - // .await; - - // let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - // let workspace = cx - // .add_window(|cx| Workspace::test_new(project, cx)) - // .root(cx); - // let finder = cx - // .add_window(|cx| { - // Picker::new( - // FileFinderDelegate::new( - // workspace.downgrade(), - // workspace.read(cx).project().clone(), - // None, - // Vec::new(), - // cx, - // ), - // cx, - // ) - // }) - // .root(cx); - // finder - // .update(cx, |f, cx| { - // f.delegate.spawn_search(test_path_like("dir"), cx) - // }) - // .await; - // cx.read(|cx| { - // let finder = finder.read(cx); - // assert_eq!(finder.delegate.matches.len(), 0); - // }); - // } - - // #[gpui::test] - // async fn test_query_history(cx: &mut gpui::TestAppContext) { - // let app_state = init_test(cx); - - // app_state - // .fs - // .as_fake() - // .insert_tree( - // "/src", - // json!({ - // "test": { - // "first.rs": "// First Rust file", - // "second.rs": "// Second Rust file", - // "third.rs": "// Third Rust file", - // } - // }), - // ) - // .await; - - // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut cx; - // let worktree_id = cx.read(|cx| { - // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - // assert_eq!(worktrees.len(), 1); - // WorktreeId::from_usize(worktrees[0].id()) - // }); - - // // Open and close panels, getting their history items afterwards. - // // Ensure history items get populated with opened items, and items are kept in a certain order. - // // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. - // // - // // TODO: without closing, the opened items do not propagate their history changes for some reason - // // it does work in real app though, only tests do not propagate. - - // let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - // assert!( - // initial_history.is_empty(), - // "Should have no history before opening any files" - // ); - - // let history_after_first = - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - // assert_eq!( - // history_after_first, - // vec![FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/first.rs")), - // }, - // Some(PathBuf::from("/src/test/first.rs")) - // )], - // "Should show 1st opened item in the history when opening the 2nd item" - // ); - - // let history_after_second = - // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; - // assert_eq!( - // history_after_second, - // vec![ - // FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/second.rs")), - // }, - // Some(PathBuf::from("/src/test/second.rs")) - // ), - // FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/first.rs")), - // }, - // Some(PathBuf::from("/src/test/first.rs")) - // ), - // ], - // "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ - // 2nd item should be the first in the history, as the last opened." - // ); - - // let history_after_third = - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - // assert_eq!( - // history_after_third, - // vec![ - // FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/third.rs")), - // }, - // Some(PathBuf::from("/src/test/third.rs")) - // ), - // FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/second.rs")), - // }, - // Some(PathBuf::from("/src/test/second.rs")) - // ), - // FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/first.rs")), - // }, - // Some(PathBuf::from("/src/test/first.rs")) - // ), - // ], - // "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ - // 3rd item should be the first in the history, as the last opened." - // ); - - // let history_after_second_again = - // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; - // assert_eq!( - // history_after_second_again, - // vec![ - // FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/second.rs")), - // }, - // Some(PathBuf::from("/src/test/second.rs")) - // ), - // FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/third.rs")), - // }, - // Some(PathBuf::from("/src/test/third.rs")) - // ), - // FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/first.rs")), - // }, - // Some(PathBuf::from("/src/test/first.rs")) - // ), - // ], - // "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ - // 2nd item, as the last opened, 3rd item should go next as it was opened right before." - // ); - // } - - // #[gpui::test] - // async fn test_external_files_history(cx: &mut gpui::TestAppContext) { - // let app_state = init_test(cx); - - // app_state - // .fs - // .as_fake() - // .insert_tree( - // "/src", - // json!({ - // "test": { - // "first.rs": "// First Rust file", - // "second.rs": "// Second Rust file", - // } - // }), - // ) - // .await; - - // app_state - // .fs - // .as_fake() - // .insert_tree( - // "/external-src", - // json!({ - // "test": { - // "third.rs": "// Third Rust file", - // "fourth.rs": "// Fourth Rust file", - // } - // }), - // ) - // .await; - - // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - // cx.update(|cx| { - // project.update(cx, |project, cx| { - // project.find_or_create_local_worktree("/external-src", false, cx) - // }) - // }) - // .detach(); - // cx.background_executor.run_until_parked(); - - // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut cx; - // let worktree_id = cx.read(|cx| { - // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - // assert_eq!(worktrees.len(), 1,); - - // WorktreeId::from_usize(worktrees[0].id()) - // }); - // workspace - // .update(cx, |workspace, cx| { - // workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) - // }) - // .detach(); - // cx.background_executor.run_until_parked(); - // let external_worktree_id = cx.read(|cx| { - // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - // assert_eq!( - // worktrees.len(), - // 2, - // "External file should get opened in a new worktree" - // ); - - // WorktreeId::from_usize( - // worktrees - // .into_iter() - // .find(|worktree| worktree.entity_id() != worktree_id.to_usize()) - // .expect("New worktree should have a different id") - // .id(), - // ) - // }); - // close_active_item(&workspace, cx).await; - - // let initial_history_items = - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - // assert_eq!( - // initial_history_items, - // vec![FoundPath::new( - // ProjectPath { - // worktree_id: external_worktree_id, - // path: Arc::from(Path::new("")), - // }, - // Some(PathBuf::from("/external-src/test/third.rs")) - // )], - // "Should show external file with its full path in the history after it was open" - // ); - - // let updated_history_items = - // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - // assert_eq!( - // updated_history_items, - // vec![ - // FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/second.rs")), - // }, - // Some(PathBuf::from("/src/test/second.rs")) - // ), - // FoundPath::new( - // ProjectPath { - // worktree_id: external_worktree_id, - // path: Arc::from(Path::new("")), - // }, - // Some(PathBuf::from("/external-src/test/third.rs")) - // ), - // ], - // "Should keep external file with history updates", - // ); - // } - - #[gpui::test] - async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let cx = &mut cx; - - // generate some history to select from - open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; - let current_history = - open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - - for expected_selected_index in 0..current_history.len() { - cx.dispatch_action(Toggle); - let selected_index = workspace.update(cx, |workspace, cx| { - workspace - .current_modal::(cx) - .unwrap() - .read(cx) - .picker - .read(cx) - .delegate - .selected_index() - }); - assert_eq!( - selected_index, expected_selected_index, - "Should select the next item in the history" - ); - } - - cx.dispatch_action(Toggle); - let selected_index = workspace.update(cx, |workspace, cx| { - workspace - .current_modal::(cx) - .unwrap() - .read(cx) - .picker - .read(cx) - .delegate - .selected_index() - }); - assert_eq!( - selected_index, 0, - "Should wrap around the history and start all over" - ); - } - - // #[gpui::test] - // async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { - // let app_state = init_test(cx); - - // app_state - // .fs - // .as_fake() - // .insert_tree( - // "/src", - // json!({ - // "test": { - // "first.rs": "// First Rust file", - // "second.rs": "// Second Rust file", - // "third.rs": "// Third Rust file", - // "fourth.rs": "// Fourth Rust file", - // } - // }), - // ) - // .await; - - // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut cx; - // let worktree_id = cx.read(|cx| { - // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - // assert_eq!(worktrees.len(), 1,); - - // WorktreeId::from_usize(worktrees[0].entity_id()) - // }); - - // // generate some history to select from - // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - - // cx.dispatch_action(Toggle); - // let first_query = "f"; - // let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - // finder - // .update(cx, |finder, cx| { - // finder.delegate.update_matches(first_query.to_string(), cx) - // }) - // .await; - // finder.read_with(cx, |finder, _| { - // let delegate = &finder.delegate; - // assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); - // let history_match = delegate.matches.history.first().unwrap(); - // assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); - // assert_eq!(history_match.0, FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/first.rs")), - // }, - // Some(PathBuf::from("/src/test/first.rs")) - // )); - // assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); - // assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); - // }); - - // let second_query = "fsdasdsa"; - // let finder = workspace.update(cx, |workspace, cx| { - // workspace - // .current_modal::(cx) - // .unwrap() - // .read(cx) - // .picker - // }); - // finder - // .update(cx, |finder, cx| { - // finder.delegate.update_matches(second_query.to_string(), cx) - // }) - // .await; - // finder.update(cx, |finder, _| { - // let delegate = &finder.delegate; - // assert!( - // delegate.matches.history.is_empty(), - // "No history entries should match {second_query}" - // ); - // assert!( - // delegate.matches.search.is_empty(), - // "No search entries should match {second_query}" - // ); - // }); - - // let first_query_again = first_query; - - // let finder = workspace.update(cx, |workspace, cx| { - // workspace - // .current_modal::(cx) - // .unwrap() - // .read(cx) - // .picker - // }); - // finder - // .update(cx, |finder, cx| { - // finder - // .delegate - // .update_matches(first_query_again.to_string(), cx) - // }) - // .await; - // finder.read_with(cx, |finder, _| { - // let delegate = &finder.delegate; - // assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); - // let history_match = delegate.matches.history.first().unwrap(); - // assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); - // assert_eq!(history_match.0, FoundPath::new( - // ProjectPath { - // worktree_id, - // path: Arc::from(Path::new("test/first.rs")), - // }, - // Some(PathBuf::from("/src/test/first.rs")) - // )); - // assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); - // assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); - // }); - // } - - // #[gpui::test] - // async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) { - // let app_state = init_test(cx); - - // app_state - // .fs - // .as_fake() - // .insert_tree( - // "/src", - // json!({ - // "collab_ui": { - // "first.rs": "// First Rust file", - // "second.rs": "// Second Rust file", - // "third.rs": "// Third Rust file", - // "collab_ui.rs": "// Fourth Rust file", - // } - // }), - // ) - // .await; - - // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut cx; - // // generate some history to select from - // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - - // cx.dispatch_action(Toggle); - // let query = "collab_ui"; - // let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - // finder - // .update(cx, |finder, cx| { - // finder.delegate.update_matches(query.to_string(), cx) - // }) - // .await; - // finder.read_with(cx, |finder, _| { - // let delegate = &finder.delegate; - // assert!( - // delegate.matches.history.is_empty(), - // "History items should not math query {query}, they should be matched by name only" - // ); - - // let search_entries = delegate - // .matches - // .search - // .iter() - // .map(|path_match| path_match.path.to_path_buf()) - // .collect::>(); - // assert_eq!( - // search_entries, - // vec![ - // PathBuf::from("collab_ui/collab_ui.rs"), - // PathBuf::from("collab_ui/third.rs"), - // PathBuf::from("collab_ui/first.rs"), - // PathBuf::from("collab_ui/second.rs"), - // ], - // "Despite all search results having the same directory name, the most matching one should be on top" - // ); - // }); - // } - - // #[gpui::test] - // async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) { - // let app_state = init_test(cx); - - // app_state - // .fs - // .as_fake() - // .insert_tree( - // "/src", - // json!({ - // "test": { - // "first.rs": "// First Rust file", - // "nonexistent.rs": "// Second Rust file", - // "third.rs": "// Third Rust file", - // } - // }), - // ) - // .await; - - // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut cx; - // // generate some history to select from - // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - // open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; - // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; - // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - - // cx.dispatch_action(Toggle); - // let query = "rs"; - // let finder = cx.read(|cx| workspace.read(cx).current_modal::().unwrap()); - // finder - // .update(cx, |finder, cx| { - // finder.picker.update(cx, |picker, cx| { - // picker.delegate.update_matches(query.to_string(), cx) - // }) - // }) - // .await; - // finder.update(cx, |finder, _| { - // let history_entries = finder.delegate - // .matches - // .history - // .iter() - // .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) - // .collect::>(); - // assert_eq!( - // history_entries, - // vec![ - // PathBuf::from("test/first.rs"), - // PathBuf::from("test/third.rs"), - // ], - // "Should have all opened files in the history, except the ones that do not exist on disk" - // ); - // }); - // } - - async fn open_close_queried_buffer( - input: &str, - expected_matches: usize, - expected_editor_title: &str, - workspace: &View, - cx: &mut gpui::VisualTestContext<'_>, - ) -> Vec { - cx.dispatch_action(Toggle); - let picker = workspace.update(cx, |workspace, cx| { - workspace - .current_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }); - picker - .update(cx, |finder, cx| { - finder.delegate.update_matches(input.to_string(), cx) - }) - .await; - let history_items = picker.update(cx, |finder, _| { - assert_eq!( - finder.delegate.matches.len(), - expected_matches, - "Unexpected number of matches found for query {input}" - ); - finder.delegate.history_items.clone() - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(SelectNext); - cx.dispatch_action(Confirm); - cx.background_executor.run_until_parked(); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - cx.read(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - let active_editor_title = active_item - .to_any() - .downcast::() - .unwrap() - .read(cx) - .title(cx); - assert_eq!( - expected_editor_title, active_editor_title, - "Unexpected editor title for query {input}" - ); - }); - - close_active_item(workspace, cx).await; - - history_items - } - - async fn close_active_item(workspace: &View, cx: &mut VisualTestContext<'_>) { - let mut original_items = HashMap::new(); - cx.read(|cx| { - for pane in workspace.read(cx).panes() { - let pane_id = pane.entity_id(); - let pane = pane.read(cx); - let insertion_result = original_items.insert(pane_id, pane.items().count()); - assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); - } - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - active_pane - .update(cx, |pane, cx| { - pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) - .unwrap() - }) - .await - .unwrap(); - cx.background_executor.run_until_parked(); - cx.read(|cx| { - for pane in workspace.read(cx).panes() { - let pane_id = pane.entity_id(); - let pane = pane.read(cx); - match original_items.remove(&pane_id) { - Some(original_items) => { - assert_eq!( - pane.items().count(), - original_items.saturating_sub(1), - "Pane id {pane_id} should have item closed" - ); - } - None => panic!("Pane id {pane_id} not found in original items"), - } - } - }); - assert!( - original_items.len() <= 1, - "At most one panel should got closed" - ); - } - - fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let state = AppState::test(cx); - theme::init(cx); - language::init(cx); - super::init(cx); - editor::init(cx); - workspace::init_settings(cx); - Project::init_settings(cx); - state - }) - } - - fn test_path_like(test_str: &str) -> PathLikeWithPosition { - PathLikeWithPosition::parse_str(test_str, |path_like_str| { - Ok::<_, std::convert::Infallible>(FileSearchQuery { - raw_query: test_str.to_owned(), - file_query_end: if path_like_str == test_str { - None - } else { - Some(path_like_str.len()) - }, - }) - }) - .unwrap() - } - - fn dummy_found_path(project_path: ProjectPath) -> FoundPath { - FoundPath { - project: project_path, - absolute: None, - } - } - - fn build_find_picker( - project: Model, - cx: &mut TestAppContext, - ) -> ( - View>, - View, - VisualTestContext, - ) { - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - cx.dispatch_action(Toggle); - let picker = workspace.update(&mut cx, |workspace, cx| { - workspace - .current_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }); - (picker, workspace, cx) - } -} +// #[cfg(test)] +// mod tests { +// use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; + +// use super::*; +// use editor::Editor; +// use gpui::{Entity, TestAppContext, VisualTestContext}; +// use menu::{Confirm, SelectNext}; +// use serde_json::json; +// use workspace::{AppState, Workspace}; + +// #[ctor::ctor] +// fn init_logger() { +// if std::env::var("RUST_LOG").is_ok() { +// env_logger::init(); +// } +// } + +// #[gpui::test] +// async fn test_matching_paths(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "banana": "", +// "bandana": "", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + +// let (picker, workspace, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// picker +// .update(cx, |picker, cx| { +// picker.delegate.update_matches("bna".to_string(), cx) +// }) +// .await; + +// picker.update(cx, |picker, _| { +// assert_eq!(picker.delegate.matches.len(), 2); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(SelectNext); +// cx.dispatch_action(Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// cx.read(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// assert_eq!( +// active_item +// .to_any() +// .downcast::() +// .unwrap() +// .read(cx) +// .title(cx), +// "bandana" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let first_file_name = "first.rs"; +// let first_file_contents = "// First Rust file"; +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// first_file_name: first_file_contents, +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + +// let (picker, workspace, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// let file_query = &first_file_name[..3]; +// let file_row = 1; +// let file_column = 3; +// assert!(file_column <= first_file_contents.len()); +// let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); +// picker +// .update(cx, |finder, cx| { +// finder +// .delegate +// .update_matches(query_inside_file.to_string(), cx) +// }) +// .await; +// picker.update(cx, |finder, _| { +// let finder = &finder.delegate; +// assert_eq!(finder.matches.len(), 1); +// let latest_search_query = finder +// .latest_search_query +// .as_ref() +// .expect("Finder should have a query after the update_matches call"); +// assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); +// assert_eq!( +// latest_search_query.path_like.file_query_end, +// Some(file_query.len()) +// ); +// assert_eq!(latest_search_query.row, Some(file_row)); +// assert_eq!(latest_search_query.column, Some(file_column as u32)); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(SelectNext); +// cx.dispatch_action(Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// let editor = cx.update(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// active_item.downcast::().unwrap() +// }); +// cx.executor().advance_clock(Duration::from_secs(2)); + +// editor.update(cx, |editor, cx| { +// let all_selections = editor.selections.all_adjusted(cx); +// assert_eq!( +// all_selections.len(), +// 1, +// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" +// ); +// let caret_selection = all_selections.into_iter().next().unwrap(); +// assert_eq!(caret_selection.start, caret_selection.end, +// "Caret selection should have its start and end at the same position"); +// assert_eq!(file_row, caret_selection.start.row + 1, +// "Query inside file should get caret with the same focus row"); +// assert_eq!(file_column, caret_selection.start.column as usize + 1, +// "Query inside file should get caret with the same focus column"); +// }); +// } + +// #[gpui::test] +// async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let first_file_name = "first.rs"; +// let first_file_contents = "// First Rust file"; +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// first_file_name: first_file_contents, +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + +// let (picker, workspace, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// let file_query = &first_file_name[..3]; +// let file_row = 200; +// let file_column = 300; +// assert!(file_column > first_file_contents.len()); +// let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); +// picker +// .update(cx, |picker, cx| { +// picker +// .delegate +// .update_matches(query_outside_file.to_string(), cx) +// }) +// .await; +// picker.update(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert_eq!(delegate.matches.len(), 1); +// let latest_search_query = delegate +// .latest_search_query +// .as_ref() +// .expect("Finder should have a query after the update_matches call"); +// assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); +// assert_eq!( +// latest_search_query.path_like.file_query_end, +// Some(file_query.len()) +// ); +// assert_eq!(latest_search_query.row, Some(file_row)); +// assert_eq!(latest_search_query.column, Some(file_column as u32)); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(SelectNext); +// cx.dispatch_action(Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// let editor = cx.update(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// active_item.downcast::().unwrap() +// }); +// cx.executor().advance_clock(Duration::from_secs(2)); + +// editor.update(cx, |editor, cx| { +// let all_selections = editor.selections.all_adjusted(cx); +// assert_eq!( +// all_selections.len(), +// 1, +// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" +// ); +// let caret_selection = all_selections.into_iter().next().unwrap(); +// assert_eq!(caret_selection.start, caret_selection.end, +// "Caret selection should have its start and end at the same position"); +// assert_eq!(0, caret_selection.start.row, +// "Excessive rows (as in query outside file borders) should get trimmed to last file row"); +// assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, +// "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); +// }); +// } + +// #[gpui::test] +// async fn test_matching_cancellation(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/dir", +// json!({ +// "hello": "", +// "goodbye": "", +// "halogen-light": "", +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; + +// let (picker, _, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// let query = test_path_like("hi"); +// picker +// .update(cx, |picker, cx| { +// picker.delegate.spawn_search(query.clone(), cx) +// }) +// .await; + +// picker.update(cx, |picker, _cx| { +// assert_eq!(picker.delegate.matches.len(), 5) +// }); + +// picker.update(cx, |picker, cx| { +// let delegate = &mut picker.delegate; +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); + +// // Simulate a search being cancelled after the time limit, +// // returning only a subset of the matches that would have been found. +// drop(delegate.spawn_search(query.clone(), cx)); +// delegate.set_search_matches( +// delegate.latest_search_id, +// true, // did-cancel +// query.clone(), +// vec![matches[1].clone(), matches[3].clone()], +// cx, +// ); + +// // Simulate another cancellation. +// drop(delegate.spawn_search(query.clone(), cx)); +// delegate.set_search_matches( +// delegate.latest_search_id, +// true, // did-cancel +// query.clone(), +// vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], +// cx, +// ); + +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); +// }); +// } + +// #[gpui::test] +// async fn test_ignored_files(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/ancestor", +// json!({ +// ".gitignore": "ignored-root", +// "ignored-root": { +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }, +// "tracked-root": { +// ".gitignore": "height", +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }, +// }), +// ) +// .await; + +// let project = Project::test( +// app_state.fs.clone(), +// [ +// "/ancestor/tracked-root".as_ref(), +// "/ancestor/ignored-root".as_ref(), +// ], +// cx, +// ) +// .await; + +// let (picker, _, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// picker +// .update(cx, |picker, cx| { +// picker.delegate.spawn_search(test_path_like("hi"), cx) +// }) +// .await; +// picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7)); +// } + +// #[gpui::test] +// async fn test_single_file_worktrees(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) +// .await; + +// let project = Project::test( +// app_state.fs.clone(), +// ["/root/the-parent-dir/the-file".as_ref()], +// cx, +// ) +// .await; + +// let (picker, _, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// // Even though there is only one worktree, that worktree's filename +// // is included in the matching, because the worktree is a single file. +// picker +// .update(cx, |picker, cx| { +// picker.delegate.spawn_search(test_path_like("thf"), cx) +// }) +// .await; +// cx.read(|cx| { +// let picker = picker.read(cx); +// let delegate = &picker.delegate; +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); +// assert_eq!(matches.len(), 1); + +// let (file_name, file_name_positions, full_path, full_path_positions) = +// delegate.labels_for_path_match(&matches[0]); +// assert_eq!(file_name, "the-file"); +// assert_eq!(file_name_positions, &[0, 1, 4]); +// assert_eq!(full_path, "the-file"); +// assert_eq!(full_path_positions, &[0, 1, 4]); +// }); + +// // Since the worktree root is a file, searching for its name followed by a slash does +// // not match anything. +// picker +// .update(cx, |f, cx| { +// f.delegate.spawn_search(test_path_like("thf/"), cx) +// }) +// .await; +// picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0)); +// } + +// #[gpui::test] +// async fn test_path_distance_ordering(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "dir1": { "a.txt": "" }, +// "dir2": { +// "a.txt": "", +// "b.txt": "" +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; + +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// WorktreeId::from_usize(worktrees[0].id()) +// }); + +// // When workspace has an active item, sort items which are closer to that item +// // first when they have the same name. In this case, b.txt is closer to dir2's a.txt +// // so that one should be sorted earlier +// let b_path = Some(dummy_found_path(ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("/root/dir2/b.txt")), +// })); +// cx.dispatch_action(Toggle); + +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// b_path, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); + +// finder +// .update(cx, |f, cx| { +// f.delegate.spawn_search(test_path_like("a.txt"), cx) +// }) +// .await; + +// finder.read_with(cx, |f, _| { +// let delegate = &f.delegate; +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); +// assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); +// assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); +// }); +// } + +// #[gpui::test] +// async fn test_search_worktree_without_files(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "dir1": {}, +// "dir2": { +// "dir3": {} +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// None, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); +// finder +// .update(cx, |f, cx| { +// f.delegate.spawn_search(test_path_like("dir"), cx) +// }) +// .await; +// cx.read(|cx| { +// let finder = finder.read(cx); +// assert_eq!(finder.delegate.matches.len(), 0); +// }); +// } + +// #[gpui::test] +// async fn test_query_history(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// WorktreeId::from_usize(worktrees[0].id()) +// }); + +// // Open and close panels, getting their history items afterwards. +// // Ensure history items get populated with opened items, and items are kept in a certain order. +// // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. +// // +// // TODO: without closing, the opened items do not propagate their history changes for some reason +// // it does work in real app though, only tests do not propagate. + +// let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// assert!( +// initial_history.is_empty(), +// "Should have no history before opening any files" +// ); + +// let history_after_first = +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// assert_eq!( +// history_after_first, +// vec![FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )], +// "Should show 1st opened item in the history when opening the 2nd item" +// ); + +// let history_after_second = +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// assert_eq!( +// history_after_second, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// ), +// ], +// "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ +// 2nd item should be the first in the history, as the last opened." +// ); + +// let history_after_third = +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// assert_eq!( +// history_after_third, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/third.rs")), +// }, +// Some(PathBuf::from("/src/test/third.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// ), +// ], +// "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ +// 3rd item should be the first in the history, as the last opened." +// ); + +// let history_after_second_again = +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// assert_eq!( +// history_after_second_again, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/third.rs")), +// }, +// Some(PathBuf::from("/src/test/third.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// ), +// ], +// "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ +// 2nd item, as the last opened, 3rd item should go next as it was opened right before." +// ); +// } + +// #[gpui::test] +// async fn test_external_files_history(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/external-src", +// json!({ +// "test": { +// "third.rs": "// Third Rust file", +// "fourth.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// cx.update(|cx| { +// project.update(cx, |project, cx| { +// project.find_or_create_local_worktree("/external-src", false, cx) +// }) +// }) +// .detach(); +// cx.background_executor.run_until_parked(); + +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1,); + +// WorktreeId::from_usize(worktrees[0].id()) +// }); +// workspace +// .update(cx, |workspace, cx| { +// workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) +// }) +// .detach(); +// cx.background_executor.run_until_parked(); +// let external_worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!( +// worktrees.len(), +// 2, +// "External file should get opened in a new worktree" +// ); + +// WorktreeId::from_usize( +// worktrees +// .into_iter() +// .find(|worktree| worktree.entity_id() != worktree_id.to_usize()) +// .expect("New worktree should have a different id") +// .id(), +// ) +// }); +// close_active_item(&workspace, cx).await; + +// let initial_history_items = +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// assert_eq!( +// initial_history_items, +// vec![FoundPath::new( +// ProjectPath { +// worktree_id: external_worktree_id, +// path: Arc::from(Path::new("")), +// }, +// Some(PathBuf::from("/external-src/test/third.rs")) +// )], +// "Should show external file with its full path in the history after it was open" +// ); + +// let updated_history_items = +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// assert_eq!( +// updated_history_items, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id: external_worktree_id, +// path: Arc::from(Path::new("")), +// }, +// Some(PathBuf::from("/external-src/test/third.rs")) +// ), +// ], +// "Should keep external file with history updates", +// ); +// } + +// #[gpui::test] +// async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; + +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// cx.executor().run_until_parked(); +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + +// for expected_selected_index in 0..current_history.len() { +// cx.dispatch_action(Toggle); +// let selected_index = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .read(cx) +// .delegate +// .selected_index() +// }); +// assert_eq!( +// selected_index, expected_selected_index, +// "Should select the next item in the history" +// ); +// } + +// cx.dispatch_action(Toggle); +// let selected_index = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .read(cx) +// .delegate +// .selected_index() +// }); +// assert_eq!( +// selected_index, 0, +// "Should wrap around the history and start all over" +// ); +// } + +// #[gpui::test] +// async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// "fourth.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1,); + +// WorktreeId::from_usize(worktrees[0].entity_id()) +// }); + +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + +// cx.dispatch_action(Toggle); +// let first_query = "f"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate.update_matches(first_query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); +// let history_match = delegate.matches.history.first().unwrap(); +// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); +// assert_eq!(history_match.0, FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )); +// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); +// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); +// }); + +// let second_query = "fsdasdsa"; +// let finder = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// }); +// finder +// .update(cx, |finder, cx| { +// finder.delegate.update_matches(second_query.to_string(), cx) +// }) +// .await; +// finder.update(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert!( +// delegate.matches.history.is_empty(), +// "No history entries should match {second_query}" +// ); +// assert!( +// delegate.matches.search.is_empty(), +// "No search entries should match {second_query}" +// ); +// }); + +// let first_query_again = first_query; + +// let finder = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// }); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate +// .update_matches(first_query_again.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); +// let history_match = delegate.matches.history.first().unwrap(); +// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); +// assert_eq!(history_match.0, FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )); +// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); +// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); +// }); +// } + +// #[gpui::test] +// async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "collab_ui": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// "collab_ui.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + +// cx.dispatch_action(Toggle); +// let query = "collab_ui"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate.update_matches(query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert!( +// delegate.matches.history.is_empty(), +// "History items should not math query {query}, they should be matched by name only" +// ); + +// let search_entries = delegate +// .matches +// .search +// .iter() +// .map(|path_match| path_match.path.to_path_buf()) +// .collect::>(); +// assert_eq!( +// search_entries, +// vec![ +// PathBuf::from("collab_ui/collab_ui.rs"), +// PathBuf::from("collab_ui/third.rs"), +// PathBuf::from("collab_ui/first.rs"), +// PathBuf::from("collab_ui/second.rs"), +// ], +// "Despite all search results having the same directory name, the most matching one should be on top" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "nonexistent.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + +// cx.dispatch_action(Toggle); +// let query = "rs"; +// let finder = cx.read(|cx| workspace.read(cx).current_modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.picker.update(cx, |picker, cx| { +// picker.delegate.update_matches(query.to_string(), cx) +// }) +// }) +// .await; +// finder.update(cx, |finder, _| { +// let history_entries = finder.delegate +// .matches +// .history +// .iter() +// .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) +// .collect::>(); +// assert_eq!( +// history_entries, +// vec![ +// PathBuf::from("test/first.rs"), +// PathBuf::from("test/third.rs"), +// ], +// "Should have all opened files in the history, except the ones that do not exist on disk" +// ); +// }); +// } + +// async fn open_close_queried_buffer( +// input: &str, +// expected_matches: usize, +// expected_editor_title: &str, +// workspace: &View, +// cx: &mut gpui::VisualTestContext<'_>, +// ) -> Vec { +// cx.dispatch_action(Toggle); +// let picker = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .clone() +// }); +// picker +// .update(cx, |finder, cx| { +// finder.delegate.update_matches(input.to_string(), cx) +// }) +// .await; +// let history_items = picker.update(cx, |finder, _| { +// assert_eq!( +// finder.delegate.matches.len(), +// expected_matches, +// "Unexpected number of matches found for query {input}" +// ); +// finder.delegate.history_items.clone() +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(SelectNext); +// cx.dispatch_action(Confirm); +// cx.background_executor.run_until_parked(); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// cx.read(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// let active_editor_title = active_item +// .to_any() +// .downcast::() +// .unwrap() +// .read(cx) +// .title(cx); +// assert_eq!( +// expected_editor_title, active_editor_title, +// "Unexpected editor title for query {input}" +// ); +// }); + +// close_active_item(workspace, cx).await; + +// history_items +// } + +// async fn close_active_item(workspace: &View, cx: &mut VisualTestContext<'_>) { +// let mut original_items = HashMap::new(); +// cx.read(|cx| { +// for pane in workspace.read(cx).panes() { +// let pane_id = pane.entity_id(); +// let pane = pane.read(cx); +// let insertion_result = original_items.insert(pane_id, pane.items().count()); +// assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); +// } +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// active_pane +// .update(cx, |pane, cx| { +// pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) +// .unwrap() +// }) +// .await +// .unwrap(); +// cx.background_executor.run_until_parked(); +// cx.read(|cx| { +// for pane in workspace.read(cx).panes() { +// let pane_id = pane.entity_id(); +// let pane = pane.read(cx); +// match original_items.remove(&pane_id) { +// Some(original_items) => { +// assert_eq!( +// pane.items().count(), +// original_items.saturating_sub(1), +// "Pane id {pane_id} should have item closed" +// ); +// } +// None => panic!("Pane id {pane_id} not found in original items"), +// } +// } +// }); +// assert!( +// original_items.len() <= 1, +// "At most one panel should got closed" +// ); +// } + +// fn init_test(cx: &mut TestAppContext) -> Arc { +// cx.update(|cx| { +// let state = AppState::test(cx); +// theme::init(cx); +// language::init(cx); +// super::init(cx); +// editor::init(cx); +// workspace::init_settings(cx); +// Project::init_settings(cx); +// state +// }) +// } + +// fn test_path_like(test_str: &str) -> PathLikeWithPosition { +// PathLikeWithPosition::parse_str(test_str, |path_like_str| { +// Ok::<_, std::convert::Infallible>(FileSearchQuery { +// raw_query: test_str.to_owned(), +// file_query_end: if path_like_str == test_str { +// None +// } else { +// Some(path_like_str.len()) +// }, +// }) +// }) +// .unwrap() +// } + +// fn dummy_found_path(project_path: ProjectPath) -> FoundPath { +// FoundPath { +// project: project_path, +// absolute: None, +// } +// } + +// fn build_find_picker( +// project: Model, +// cx: &mut TestAppContext, +// ) -> ( +// View>, +// View, +// VisualTestContext, +// ) { +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// cx.dispatch_action(Toggle); +// let picker = workspace.update(&mut cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .clone() +// }); +// (picker, workspace, cx) +// } +// } diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 6687559d1c..c81ff5f26a 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -15,7 +15,7 @@ use taffy::style::Overflow; pub fn uniform_list( id: Id, item_count: usize, - f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> SmallVec<[C; 64]>, + f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, ) -> UniformList where Id: Into, diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 1feead1a19..b39d62c9a1 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -21,7 +21,6 @@ use project::{ }; use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; use std::{ cmp::Ordering, collections::{hash_map, HashMap}, @@ -1468,7 +1467,7 @@ impl Render for ProjectPanel { .map(|(_, worktree_entries)| worktree_entries.len()) .sum(), |this: &mut Self, range, cx| { - let mut items = SmallVec::new(); + let mut items = Vec::new(); this.for_each_visible_entry(range, cx, |id, details, cx| { items.push(this.render_entry(id, details, cx)); }); diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 4786e7e35d..0101b60f88 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -38,10 +38,10 @@ use futures::{ use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, FocusableKeyDispatch, GlobalPixels, KeyContext, Model, ModelContext, - ParentElement, Point, Render, Size, StatefulInteractive, StatefulInteractivity, - StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, View, ViewContext, - VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, + FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, + StatefulInteractive, StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, + View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, + WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3037,10 +3037,10 @@ impl Workspace { fn force_remove_pane(&mut self, pane: &View, cx: &mut ViewContext) { self.panes.retain(|p| p != pane); - if true { - todo!() - // cx.focus(self.panes.last().unwrap()); - } + self.panes + .last() + .unwrap() + .update(cx, |pane, cx| pane.focus(cx)); if self.last_active_center_pane == Some(pane.downgrade()) { self.last_active_center_pane = None; } @@ -3429,8 +3429,7 @@ impl Workspace { node_runtime: FakeNodeRuntime::new(), }); let workspace = Self::new(0, project, app_state, cx); - dbg!(&workspace.focus_handle); - workspace.focus_handle.focus(cx); + workspace.active_pane.update(cx, |pane, cx| pane.focus(cx)); workspace } @@ -3709,7 +3708,7 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA impl EventEmitter for Workspace {} impl Render for Workspace { - type Element = Div, FocusableKeyDispatch>; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); @@ -3717,7 +3716,6 @@ impl Render for Workspace { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); self.add_workspace_actions_listeners(div()) - .track_focus(&self.focus_handle) .context(context) .relative() .size_full() From 817c6dd49cb141990a3508fe6c07331c404b4f80 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 15:57:23 -0800 Subject: [PATCH 118/126] remove dbg --- crates/file_finder2/src/file_finder.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 39bbb91465..aae3bca160 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -33,9 +33,7 @@ pub fn init(cx: &mut AppContext) { impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { - dbg!("REGISTERING"); workspace.register_action(|workspace, _: &Toggle, cx| { - dbg!("CALLING ACTION"); let Some(file_finder) = workspace.current_modal::(cx) else { Self::open(workspace, cx); return; @@ -594,7 +592,6 @@ impl PickerDelegate for FileFinderDelegate { } fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { - dbg!("CONFIRMING???"); if let Some(m) = self.matches.get(self.selected_index()) { if let Some(workspace) = self.workspace.upgrade() { let open_task = workspace.update(cx, move |workspace, cx| { @@ -692,7 +689,6 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } - dbg!("DISMISSING"); finder .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed)) .ok()?; From 8de86151768cbcf7bc84c7cd7fb318b732463fdc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Nov 2023 17:19:10 -0800 Subject: [PATCH 119/126] Fix uncached raster_bounds computation and font selection Co-authored-by: Nathan Sobo Co-authored-by: Mikayla --- crates/gpui2/src/platform.rs | 6 ++++- crates/gpui2/src/platform/mac/text_system.rs | 15 ++++++++--- crates/gpui2/src/text_system.rs | 26 ++++++++++++++------ 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 8b49addec9..00ce3340f8 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -184,7 +184,11 @@ pub trait PlatformTextSystem: Send + Sync { fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result>; - fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size, Vec)>; + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)>; fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout; fn wrap_line( &self, diff --git a/crates/gpui2/src/platform/mac/text_system.rs b/crates/gpui2/src/platform/mac/text_system.rs index b87db09dc0..155f3097fe 100644 --- a/crates/gpui2/src/platform/mac/text_system.rs +++ b/crates/gpui2/src/platform/mac/text_system.rs @@ -116,7 +116,9 @@ impl PlatformTextSystem for MacTextSystem { }, )?; - Ok(candidates[ix]) + let font_id = candidates[ix]; + lock.font_selections.insert(font.clone(), font_id); + Ok(font_id) } } @@ -145,8 +147,9 @@ impl PlatformTextSystem for MacTextSystem { fn rasterize_glyph( &self, glyph_id: &RenderGlyphParams, + raster_bounds: Bounds, ) -> Result<(Size, Vec)> { - self.0.read().rasterize_glyph(glyph_id) + self.0.read().rasterize_glyph(glyph_id, raster_bounds) } fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { @@ -247,8 +250,11 @@ impl MacTextSystemState { .into()) } - fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size, Vec)> { - let glyph_bounds = self.raster_bounds(params)?; + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result<(Size, Vec)> { if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { Err(anyhow!("glyph bounds are empty")) } else { @@ -260,6 +266,7 @@ impl MacTextSystemState { if params.subpixel_variant.y > 0 { bitmap_size.height += DevicePixels(1); } + let bitmap_size = bitmap_size; let mut bytes; let cx; diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index e8d6acc5a3..c7031fcb4d 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -39,6 +39,7 @@ pub struct TextSystem { platform_text_system: Arc, font_ids_by_font: RwLock>, font_metrics: RwLock>, + raster_bounds: RwLock>>, wrapper_pool: Mutex>>, font_runs_pool: Mutex>>, } @@ -48,10 +49,11 @@ impl TextSystem { TextSystem { line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())), platform_text_system, - font_metrics: RwLock::new(HashMap::default()), - font_ids_by_font: RwLock::new(HashMap::default()), - wrapper_pool: Mutex::new(HashMap::default()), - font_runs_pool: Default::default(), + font_metrics: RwLock::default(), + raster_bounds: RwLock::default(), + font_ids_by_font: RwLock::default(), + wrapper_pool: Mutex::default(), + font_runs_pool: Mutex::default(), } } @@ -252,14 +254,24 @@ impl TextSystem { } pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { - self.platform_text_system.glyph_raster_bounds(params) + let raster_bounds = self.raster_bounds.upgradable_read(); + if let Some(bounds) = raster_bounds.get(params) { + Ok(bounds.clone()) + } else { + let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds); + let bounds = self.platform_text_system.glyph_raster_bounds(params)?; + raster_bounds.insert(params.clone(), bounds); + Ok(bounds) + } } pub fn rasterize_glyph( &self, - glyph_id: &RenderGlyphParams, + params: &RenderGlyphParams, ) -> Result<(Size, Vec)> { - self.platform_text_system.rasterize_glyph(glyph_id) + let raster_bounds = self.raster_bounds(params)?; + self.platform_text_system + .rasterize_glyph(params, raster_bounds) } } From 00d8921ae9b10cae8762995f9d7985c84a271dd2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 19:22:41 -0700 Subject: [PATCH 120/126] Fix click events by notifying when we assign pending_mouse_down --- crates/gpui2/src/elements/div.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index f3f6385503..a08d1619ae 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -597,7 +597,7 @@ impl ParentComponent for Div { } impl Element for Div { - type ElementState = NodeState; + type ElementState = DivState; fn element_id(&self) -> Option { self.interactivity.element_id.clone() @@ -617,7 +617,7 @@ impl Element for Div { child.initialize(view_state, cx); } - NodeState { + DivState { interactive_state, child_layout_ids: SmallVec::new(), } @@ -706,7 +706,7 @@ impl Component for Div { } } -pub struct NodeState { +pub struct DivState { child_layout_ids: SmallVec<[LayoutId; 4]>, interactive_state: InteractiveElementState, } @@ -911,11 +911,13 @@ where } } *pending_mouse_down.lock() = None; + cx.notify(); }); } else { - cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| { + cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { *pending_mouse_down.lock() = Some(event.clone()); + cx.notify(); } }); } From 32ad486a8e7e658c3c12ca29bf73c53b4cfb0dd9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Nov 2023 19:52:51 -0700 Subject: [PATCH 121/126] Document contexts --- crates/gpui2/docs/contexts.md | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/gpui2/docs/contexts.md b/crates/gpui2/docs/contexts.md index e69de29bb2..763a902517 100644 --- a/crates/gpui2/docs/contexts.md +++ b/crates/gpui2/docs/contexts.md @@ -0,0 +1,41 @@ +# Contexts + +GPUI makes extensive use of *context parameters*, typically named `cx` and positioned at the end of the parameter list, unless they're before a final function parameter. A context reference provides access to application state and services. + +There are multiple kinds of contexts, and contexts implement the `Deref` trait so that a function taking `&mut AppContext` could be passed a `&mut WindowContext` or `&mut ViewContext` instead. + +``` + AppContext + / \ +ModelContext WindowContext + / + ViewContext +``` + +- The `AppContext` forms the root of the hierarchy +- `ModelContext` and `WindowContext` both dereference to `AppContext` +- `ViewContext` dereferences to `WindowContext` + +## `AppContext` + +Provides access to the global application state. All other kinds of contexts ultimately deref to an `AppContext`. You can update a `Model` by passing an `AppContext`, but you can't update a view. For that you need a `WindowContext`... + +## `WindowContext` + +Provides access to the state of an application window, and also derefs to an `AppContext`, so you can pass a window context reference to any method taking an app context. Obtain this context by calling `WindowHandle::update`. + +## `ModelContext` + +Available when you create or update a `Model`. It derefs to an `AppContext`, but also contains methods specific to the particular model, such as the ability to notify change observers or emit events. + +## `ViewContext` + +Available when you create or update a `View`. It derefs to a `WindowContext`, but also contains methods specific to the particular view, such as the ability to notify change observers or emit events. + +## `AsyncAppContext` and `AsyncWindowContext` + +Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with `Model`s or `View`s with an async context, the calls become fallible, because the context may outlive the window or even the app itself. + +## `TestAppContext` and `TestVisualContext` + +These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests. From 1def355d4467021abfcc87bef05f135ac2d1750b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 09:10:46 +0100 Subject: [PATCH 122/126] Don't return `Result` from `TextStyle::highlight` --- crates/editor2/src/display_map.rs | 7 +------ crates/editor2/src/editor.rs | 23 +++++++++-------------- crates/editor2/src/element.rs | 6 +----- crates/gpui2/src/style.rs | 4 ++-- 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index f808ffa702..d88daaccc1 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -578,12 +578,7 @@ impl DisplaySnapshot { line.push_str(chunk.chunk); let text_style = if let Some(style) = chunk.style { - editor_style - .text - .clone() - .highlight(style) - .map(Cow::Owned) - .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text)) + Cow::Owned(editor_style.text.clone().highlight(style)) } else { Cow::Borrowed(&editor_style.text) }; diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 23dfc9b9d3..b37c5e5756 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -7798,20 +7798,15 @@ impl Editor { render: Arc::new({ let rename_editor = rename_editor.clone(); move |cx: &mut BlockContext| { - let text_style = if let Some(highlight_style) = old_highlight_id + let mut text_style = cx.editor_style.text.clone(); + if let Some(highlight_style) = old_highlight_id .and_then(|h| h.style(&cx.editor_style.syntax)) { - cx.editor_style - .text - .clone() - .highlight(highlight_style) - .unwrap_or_else(|_| cx.editor_style.text.clone()) - } else { - cx.editor_style.text.clone() - }; - div().pl(cx.anchor_x).child(with_view( - &rename_editor, - |_, _| { + text_style = text_style.highlight(highlight_style); + } + div() + .pl(cx.anchor_x) + .child(with_view(&rename_editor, |_, _| { EditorElement::new(EditorStyle { background: cx.theme().system().transparent, local_player: cx.editor_style.local_player, @@ -7823,8 +7818,8 @@ impl Editor { .diagnostic_style .clone(), }) - }, - )) + })) + .render() } }), disposition: BlockDisposition::Below, diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 5cdedd1de7..1ddc5ce771 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2253,11 +2253,7 @@ impl LineWithInvisibles { if !line_chunk.is_empty() && !line_exceeded_max_len { let text_style = if let Some(style) = highlighted_chunk.style { - text_style - .clone() - .highlight(style) - .map(Cow::Owned) - .unwrap_or_else(|_| Cow::Borrowed(text_style)) + Cow::Owned(text_style.clone().highlight(style)) } else { Cow::Borrowed(text_style) }; diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 664cc61f8a..0819ba9255 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -157,7 +157,7 @@ impl Default for TextStyle { } impl TextStyle { - pub fn highlight(mut self, style: HighlightStyle) -> Result { + pub fn highlight(mut self, style: HighlightStyle) -> Self { if let Some(weight) = style.font_weight { self.font_weight = weight; } @@ -177,7 +177,7 @@ impl TextStyle { self.underline = Some(underline); } - Ok(self) + self } pub fn font(&self) -> Font { From c3094b7c3de560b6ee9f5e07b1792c910d43204d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 09:45:23 +0100 Subject: [PATCH 123/126] Introduce gpui::render_view --- crates/editor2/src/editor.rs | 13 +++--- crates/gpui2/src/style.rs | 4 +- crates/gpui2/src/view.rs | 82 +++++++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index b37c5e5756..e5c7b0e4a2 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,9 +39,9 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext, - AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, Entity, - EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, + action, actions, div, point, px, relative, rems, render_view, size, uniform_list, AnyElement, + AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, + Entity, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, StatefulInteractive, StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, @@ -7806,7 +7806,8 @@ impl Editor { } div() .pl(cx.anchor_x) - .child(with_view(&rename_editor, |_, _| { + .child(render_view( + &rename_editor, EditorElement::new(EditorStyle { background: cx.theme().system().transparent, local_player: cx.editor_style.local_player, @@ -7817,8 +7818,8 @@ impl Editor { .editor_style .diagnostic_style .clone(), - }) - })) + }), + )) .render() } }), diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 0819ba9255..20972ca846 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -1,8 +1,8 @@ use crate::{ black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, - FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Result, - Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext, + FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, + SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext, }; use refineable::{Cascade, Refineable}; use smallvec::SmallVec; diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index d12d84f43b..38120a3e37 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,7 +1,7 @@ use crate::{ private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, - Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, Model, Pixels, - Size, ViewContext, VisualContext, WeakModel, WindowContext, + BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, + Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; use std::{ @@ -281,6 +281,84 @@ where } } +pub struct RenderView { + view: View, + component: Option, +} + +impl Component for RenderView +where + C: 'static + Component, + ParentViewState: 'static, + ViewState: 'static, +{ + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + +impl Element for RenderView +where + C: 'static + Component, + ParentViewState: 'static, + ViewState: 'static, +{ + type ElementState = AnyElement; + + fn id(&self) -> Option { + Some(self.view.entity_id().into()) + } + + fn initialize( + &mut self, + _: &mut ParentViewState, + _: Option, + cx: &mut ViewContext, + ) -> Self::ElementState { + cx.with_element_id(self.view.entity_id(), |_, cx| { + self.view.update(cx, |view, cx| { + let mut element = self.component.take().unwrap().render(); + element.initialize(view, cx); + element + }) + }) + } + + fn layout( + &mut self, + _: &mut ParentViewState, + element: &mut Self::ElementState, + cx: &mut ViewContext, + ) -> LayoutId { + cx.with_element_id(self.view.entity_id(), |_, cx| { + self.view.update(cx, |view, cx| element.layout(view, cx)) + }) + } + + fn paint( + &mut self, + _: Bounds, + _: &mut ParentViewState, + element: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + cx.with_element_id(self.view.entity_id(), |_, cx| { + self.view.update(cx, |view, cx| element.paint(view, cx)) + }) + } +} + +pub fn render_view(view: &View, component: C) -> RenderView +where + C: 'static + Component, + V: 'static, +{ + RenderView { + view: view.clone(), + component: Some(component), + } +} + mod any_view { use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext}; use std::any::Any; From 003e4bc241945c14d39ad7cf63093e887d9ac916 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 09:53:32 +0100 Subject: [PATCH 124/126] Extract out a register_actions function --- crates/editor2/src/element.rs | 344 +++++++++++++++++----------------- 1 file changed, 174 insertions(+), 170 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 289834b502..d11408c0ce 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2456,176 +2456,7 @@ impl Element for EditorElement { dispatch_context, Some(editor.focus_handle.clone()), |_, cx| { - register_action(cx, Editor::move_left); - register_action(cx, Editor::move_right); - register_action(cx, Editor::move_down); - register_action(cx, Editor::move_up); - // on_action(cx, Editor::new_file); todo!() - // on_action(cx, Editor::new_file_in_direction); todo!() - register_action(cx, Editor::cancel); - register_action(cx, Editor::newline); - register_action(cx, Editor::newline_above); - register_action(cx, Editor::newline_below); - register_action(cx, Editor::backspace); - register_action(cx, Editor::delete); - register_action(cx, Editor::tab); - register_action(cx, Editor::tab_prev); - register_action(cx, Editor::indent); - register_action(cx, Editor::outdent); - register_action(cx, Editor::delete_line); - register_action(cx, Editor::join_lines); - register_action(cx, Editor::sort_lines_case_sensitive); - register_action(cx, Editor::sort_lines_case_insensitive); - register_action(cx, Editor::reverse_lines); - register_action(cx, Editor::shuffle_lines); - register_action(cx, Editor::convert_to_upper_case); - register_action(cx, Editor::convert_to_lower_case); - register_action(cx, Editor::convert_to_title_case); - register_action(cx, Editor::convert_to_snake_case); - register_action(cx, Editor::convert_to_kebab_case); - register_action(cx, Editor::convert_to_upper_camel_case); - register_action(cx, Editor::convert_to_lower_camel_case); - register_action(cx, Editor::delete_to_previous_word_start); - register_action(cx, Editor::delete_to_previous_subword_start); - register_action(cx, Editor::delete_to_next_word_end); - register_action(cx, Editor::delete_to_next_subword_end); - register_action(cx, Editor::delete_to_beginning_of_line); - register_action(cx, Editor::delete_to_end_of_line); - register_action(cx, Editor::cut_to_end_of_line); - register_action(cx, Editor::duplicate_line); - register_action(cx, Editor::move_line_up); - register_action(cx, Editor::move_line_down); - register_action(cx, Editor::transpose); - register_action(cx, Editor::cut); - register_action(cx, Editor::copy); - register_action(cx, Editor::paste); - register_action(cx, Editor::undo); - register_action(cx, Editor::redo); - register_action(cx, Editor::move_page_up); - register_action(cx, Editor::move_page_down); - register_action(cx, Editor::next_screen); - register_action(cx, Editor::scroll_cursor_top); - register_action(cx, Editor::scroll_cursor_center); - register_action(cx, Editor::scroll_cursor_bottom); - register_action(cx, |editor, _: &LineDown, cx| { - editor.scroll_screen(&ScrollAmount::Line(1.), cx) - }); - register_action(cx, |editor, _: &LineUp, cx| { - editor.scroll_screen(&ScrollAmount::Line(-1.), cx) - }); - register_action(cx, |editor, _: &HalfPageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(0.5), cx) - }); - register_action(cx, |editor, _: &HalfPageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) - }); - register_action(cx, |editor, _: &PageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(1.), cx) - }); - register_action(cx, |editor, _: &PageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-1.), cx) - }); - register_action(cx, Editor::move_to_previous_word_start); - register_action(cx, Editor::move_to_previous_subword_start); - register_action(cx, Editor::move_to_next_word_end); - register_action(cx, Editor::move_to_next_subword_end); - register_action(cx, Editor::move_to_beginning_of_line); - register_action(cx, Editor::move_to_end_of_line); - register_action(cx, Editor::move_to_start_of_paragraph); - register_action(cx, Editor::move_to_end_of_paragraph); - register_action(cx, Editor::move_to_beginning); - register_action(cx, Editor::move_to_end); - register_action(cx, Editor::select_up); - register_action(cx, Editor::select_down); - register_action(cx, Editor::select_left); - register_action(cx, Editor::select_right); - register_action(cx, Editor::select_to_previous_word_start); - register_action(cx, Editor::select_to_previous_subword_start); - register_action(cx, Editor::select_to_next_word_end); - register_action(cx, Editor::select_to_next_subword_end); - register_action(cx, Editor::select_to_beginning_of_line); - register_action(cx, Editor::select_to_end_of_line); - register_action(cx, Editor::select_to_start_of_paragraph); - register_action(cx, Editor::select_to_end_of_paragraph); - register_action(cx, Editor::select_to_beginning); - register_action(cx, Editor::select_to_end); - register_action(cx, Editor::select_all); - register_action(cx, |editor, action, cx| { - editor.select_all_matches(action, cx).log_err(); - }); - register_action(cx, Editor::select_line); - register_action(cx, Editor::split_selection_into_lines); - register_action(cx, Editor::add_selection_above); - register_action(cx, Editor::add_selection_below); - register_action(cx, |editor, action, cx| { - editor.select_next(action, cx).log_err(); - }); - register_action(cx, |editor, action, cx| { - editor.select_previous(action, cx).log_err(); - }); - register_action(cx, Editor::toggle_comments); - register_action(cx, Editor::select_larger_syntax_node); - register_action(cx, Editor::select_smaller_syntax_node); - register_action(cx, Editor::move_to_enclosing_bracket); - register_action(cx, Editor::undo_selection); - register_action(cx, Editor::redo_selection); - register_action(cx, Editor::go_to_diagnostic); - register_action(cx, Editor::go_to_prev_diagnostic); - register_action(cx, Editor::go_to_hunk); - register_action(cx, Editor::go_to_prev_hunk); - register_action(cx, Editor::go_to_definition); - register_action(cx, Editor::go_to_definition_split); - register_action(cx, Editor::go_to_type_definition); - register_action(cx, Editor::go_to_type_definition_split); - register_action(cx, Editor::fold); - register_action(cx, Editor::fold_at); - register_action(cx, Editor::unfold_lines); - register_action(cx, Editor::unfold_at); - register_action(cx, Editor::fold_selected_ranges); - register_action(cx, Editor::show_completions); - register_action(cx, Editor::toggle_code_actions); - // on_action(cx, Editor::open_excerpts); todo!() - register_action(cx, Editor::toggle_soft_wrap); - register_action(cx, Editor::toggle_inlay_hints); - register_action(cx, Editor::reveal_in_finder); - register_action(cx, Editor::copy_path); - register_action(cx, Editor::copy_relative_path); - register_action(cx, Editor::copy_highlight_json); - register_action(cx, |editor, action, cx| { - editor - .format(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, Editor::restart_language_server); - register_action(cx, Editor::show_character_palette); - // on_action(cx, Editor::confirm_completion); todo!() - register_action(cx, |editor, action, cx| { - editor - .confirm_code_action(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, |editor, action, cx| { - editor - .rename(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, |editor, action, cx| { - editor - .confirm_rename(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, |editor, action, cx| { - editor - .find_all_references(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, Editor::next_copilot_suggestion); - register_action(cx, Editor::previous_copilot_suggestion); - register_action(cx, Editor::copilot_suggest); - register_action(cx, Editor::context_menu_first); - register_action(cx, Editor::context_menu_prev); - register_action(cx, Editor::context_menu_next); - register_action(cx, Editor::context_menu_last); + register_actions(cx); // We call with_z_index to establish a new stacking context. cx.with_z_index(0, |cx| { @@ -4138,6 +3969,179 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { // } // } +fn register_actions(cx: &mut ViewContext) { + register_action(cx, Editor::move_left); + register_action(cx, Editor::move_right); + register_action(cx, Editor::move_down); + register_action(cx, Editor::move_up); + // on_action(cx, Editor::new_file); todo!() + // on_action(cx, Editor::new_file_in_direction); todo!() + register_action(cx, Editor::cancel); + register_action(cx, Editor::newline); + register_action(cx, Editor::newline_above); + register_action(cx, Editor::newline_below); + register_action(cx, Editor::backspace); + register_action(cx, Editor::delete); + register_action(cx, Editor::tab); + register_action(cx, Editor::tab_prev); + register_action(cx, Editor::indent); + register_action(cx, Editor::outdent); + register_action(cx, Editor::delete_line); + register_action(cx, Editor::join_lines); + register_action(cx, Editor::sort_lines_case_sensitive); + register_action(cx, Editor::sort_lines_case_insensitive); + register_action(cx, Editor::reverse_lines); + register_action(cx, Editor::shuffle_lines); + register_action(cx, Editor::convert_to_upper_case); + register_action(cx, Editor::convert_to_lower_case); + register_action(cx, Editor::convert_to_title_case); + register_action(cx, Editor::convert_to_snake_case); + register_action(cx, Editor::convert_to_kebab_case); + register_action(cx, Editor::convert_to_upper_camel_case); + register_action(cx, Editor::convert_to_lower_camel_case); + register_action(cx, Editor::delete_to_previous_word_start); + register_action(cx, Editor::delete_to_previous_subword_start); + register_action(cx, Editor::delete_to_next_word_end); + register_action(cx, Editor::delete_to_next_subword_end); + register_action(cx, Editor::delete_to_beginning_of_line); + register_action(cx, Editor::delete_to_end_of_line); + register_action(cx, Editor::cut_to_end_of_line); + register_action(cx, Editor::duplicate_line); + register_action(cx, Editor::move_line_up); + register_action(cx, Editor::move_line_down); + register_action(cx, Editor::transpose); + register_action(cx, Editor::cut); + register_action(cx, Editor::copy); + register_action(cx, Editor::paste); + register_action(cx, Editor::undo); + register_action(cx, Editor::redo); + register_action(cx, Editor::move_page_up); + register_action(cx, Editor::move_page_down); + register_action(cx, Editor::next_screen); + register_action(cx, Editor::scroll_cursor_top); + register_action(cx, Editor::scroll_cursor_center); + register_action(cx, Editor::scroll_cursor_bottom); + register_action(cx, |editor, _: &LineDown, cx| { + editor.scroll_screen(&ScrollAmount::Line(1.), cx) + }); + register_action(cx, |editor, _: &LineUp, cx| { + editor.scroll_screen(&ScrollAmount::Line(-1.), cx) + }); + register_action(cx, |editor, _: &HalfPageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(0.5), cx) + }); + register_action(cx, |editor, _: &HalfPageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) + }); + register_action(cx, |editor, _: &PageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.), cx) + }); + register_action(cx, |editor, _: &PageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-1.), cx) + }); + register_action(cx, Editor::move_to_previous_word_start); + register_action(cx, Editor::move_to_previous_subword_start); + register_action(cx, Editor::move_to_next_word_end); + register_action(cx, Editor::move_to_next_subword_end); + register_action(cx, Editor::move_to_beginning_of_line); + register_action(cx, Editor::move_to_end_of_line); + register_action(cx, Editor::move_to_start_of_paragraph); + register_action(cx, Editor::move_to_end_of_paragraph); + register_action(cx, Editor::move_to_beginning); + register_action(cx, Editor::move_to_end); + register_action(cx, Editor::select_up); + register_action(cx, Editor::select_down); + register_action(cx, Editor::select_left); + register_action(cx, Editor::select_right); + register_action(cx, Editor::select_to_previous_word_start); + register_action(cx, Editor::select_to_previous_subword_start); + register_action(cx, Editor::select_to_next_word_end); + register_action(cx, Editor::select_to_next_subword_end); + register_action(cx, Editor::select_to_beginning_of_line); + register_action(cx, Editor::select_to_end_of_line); + register_action(cx, Editor::select_to_start_of_paragraph); + register_action(cx, Editor::select_to_end_of_paragraph); + register_action(cx, Editor::select_to_beginning); + register_action(cx, Editor::select_to_end); + register_action(cx, Editor::select_all); + register_action(cx, |editor, action, cx| { + editor.select_all_matches(action, cx).log_err(); + }); + register_action(cx, Editor::select_line); + register_action(cx, Editor::split_selection_into_lines); + register_action(cx, Editor::add_selection_above); + register_action(cx, Editor::add_selection_below); + register_action(cx, |editor, action, cx| { + editor.select_next(action, cx).log_err(); + }); + register_action(cx, |editor, action, cx| { + editor.select_previous(action, cx).log_err(); + }); + register_action(cx, Editor::toggle_comments); + register_action(cx, Editor::select_larger_syntax_node); + register_action(cx, Editor::select_smaller_syntax_node); + register_action(cx, Editor::move_to_enclosing_bracket); + register_action(cx, Editor::undo_selection); + register_action(cx, Editor::redo_selection); + register_action(cx, Editor::go_to_diagnostic); + register_action(cx, Editor::go_to_prev_diagnostic); + register_action(cx, Editor::go_to_hunk); + register_action(cx, Editor::go_to_prev_hunk); + register_action(cx, Editor::go_to_definition); + register_action(cx, Editor::go_to_definition_split); + register_action(cx, Editor::go_to_type_definition); + register_action(cx, Editor::go_to_type_definition_split); + register_action(cx, Editor::fold); + register_action(cx, Editor::fold_at); + register_action(cx, Editor::unfold_lines); + register_action(cx, Editor::unfold_at); + register_action(cx, Editor::fold_selected_ranges); + register_action(cx, Editor::show_completions); + register_action(cx, Editor::toggle_code_actions); + // on_action(cx, Editor::open_excerpts); todo!() + register_action(cx, Editor::toggle_soft_wrap); + register_action(cx, Editor::toggle_inlay_hints); + register_action(cx, Editor::reveal_in_finder); + register_action(cx, Editor::copy_path); + register_action(cx, Editor::copy_relative_path); + register_action(cx, Editor::copy_highlight_json); + register_action(cx, |editor, action, cx| { + editor + .format(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, Editor::restart_language_server); + register_action(cx, Editor::show_character_palette); + // on_action(cx, Editor::confirm_completion); todo!() + register_action(cx, |editor, action, cx| { + editor + .confirm_code_action(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, |editor, action, cx| { + editor + .rename(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, |editor, action, cx| { + editor + .confirm_rename(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, |editor, action, cx| { + editor + .find_all_references(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, Editor::next_copilot_suggestion); + register_action(cx, Editor::previous_copilot_suggestion); + register_action(cx, Editor::copilot_suggest); + register_action(cx, Editor::context_menu_first); + register_action(cx, Editor::context_menu_prev); + register_action(cx, Editor::context_menu_next); + register_action(cx, Editor::context_menu_last); +} + fn register_action( cx: &mut ViewContext, listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, From 45381e566c1497d929c1237285c730cc8ff4847a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 11:05:09 +0100 Subject: [PATCH 125/126] Fix focus management on editor when renaming --- crates/editor2/src/editor.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 7edf8edc8c..e3d9440933 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1880,10 +1880,8 @@ impl Editor { ); let focus_handle = cx.focus_handle(); - cx.on_focus_in(&focus_handle, Self::handle_focus_in) - .detach(); - cx.on_focus_out(&focus_handle, Self::handle_focus_out) - .detach(); + cx.on_focus(&focus_handle, Self::handle_focus).detach(); + cx.on_blur(&focus_handle, Self::handle_blur).detach(); let mut this = Self { handle: cx.view().downgrade(), @@ -7903,6 +7901,10 @@ impl Editor { cx: &mut ViewContext, ) -> Option { let rename = self.pending_rename.take()?; + if rename.editor.focus_handle(cx).is_focused(cx) { + cx.focus(&self.focus_handle); + } + self.remove_blocks( [rename.block_id].into_iter().collect(), Some(Autoscroll::fit()), @@ -9201,17 +9203,13 @@ impl Editor { self.focus_handle.is_focused(cx) } - fn handle_focus_in(&mut self, cx: &mut ViewContext) { - if self.focus_handle.is_focused(cx) { - // todo!() - // let focused_event = EditorFocused(cx.handle()); - // cx.emit_global(focused_event); - cx.emit(Event::Focused); - } + fn handle_focus(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Focused); + if let Some(rename) = self.pending_rename.as_ref() { let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone(); cx.focus(&rename_editor_focus_handle); - } else if self.focus_handle.is_focused(cx) { + } else { self.blink_manager.update(cx, BlinkManager::enable); self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); @@ -9227,7 +9225,7 @@ impl Editor { } } - fn handle_focus_out(&mut self, cx: &mut ViewContext) { + fn handle_blur(&mut self, cx: &mut ViewContext) { // todo!() // let blurred_event = EditorBlurred(cx.handle()); // cx.emit_global(blurred_event); From 786cc59d7a1a918cc97f283637a5f9fe789587ed Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 11:07:32 +0100 Subject: [PATCH 126/126] Fix formatting --- crates/gpui2/src/style.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 35b08b84c2..5d9dd5d804 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -1,8 +1,8 @@ use crate::{ black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, - FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, - Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, + FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, + SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, }; use refineable::{Cascade, Refineable}; use smallvec::SmallVec;