diff --git a/Cargo.lock b/Cargo.lock index 23a87887a5e7f2c179881355cc854a093985eba7..6d7ce98341f0dab435d1148710f2134ebae582d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.42" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "arrayref" @@ -3329,7 +3329,10 @@ dependencies = [ name = "project_panel" version = "0.1.0" dependencies = [ + "editor", + "futures", "gpui", + "postage", "project", "serde_json", "settings", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1378022bcf4b46f698e373df3df3c8544d4bef63..dd83b91ed06f95361349d340e2c708f8145acc46 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -331,7 +331,9 @@ "context": "ProjectPanel", "bindings": { "left": "project_panel::CollapseSelectedEntry", - "right": "project_panel::ExpandSelectedEntry" + "right": "project_panel::ExpandSelectedEntry", + "f2": "project_panel::Rename", + "backspace": "project_panel::Delete" } } ] \ No newline at end of file diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 4b53480c7956768c91cc36108d00c7eff2e43309..3b19c2e63eef855981632972fe933324dd677902 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -937,6 +937,7 @@ "top": 6, "bottom": 6 }, + "indent_width": 20, "entry": { "height": 24, "icon_color": "#8b8792", @@ -946,41 +947,42 @@ "family": "Zed Mono", "color": "#8b8792", "size": 14 + }, + "hover": { + "background": "#5852603d", + "text": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14 + } + }, + "active": { + "background": "#5852605c", + "text": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14 + } + }, + "active_hover": { + "background": "#5852603d", + "text": { + "family": "Zed Mono", + "color": "#efecf4", + "size": 14 + } } }, - "hovered_entry": { - "height": 24, - "background": "#5852603d", - "icon_color": "#8b8792", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#e2dfe7", - "size": 14 - } - }, - "selected_entry": { - "height": 24, - "icon_color": "#8b8792", - "icon_size": 8, - "icon_spacing": 8, + "filename_editor": { + "background": "#26232a5c", "text": { "family": "Zed Mono", "color": "#e2dfe7", "size": 14 - } - }, - "hovered_selected_entry": { - "height": 24, - "background": "#5852603d", - "icon_color": "#8b8792", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#efecf4", - "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" } } }, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 54e4e6eac841ba2dac9b98d6644dffa3b4a50ed2..2e33fb774fe95b06c4da7d8d7d6209c20673b592 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -937,6 +937,7 @@ "top": 6, "bottom": 6 }, + "indent_width": 20, "entry": { "height": 24, "icon_color": "#585260", @@ -946,41 +947,42 @@ "family": "Zed Mono", "color": "#585260", "size": 14 + }, + "hover": { + "background": "#8b87921f", + "text": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14 + } + }, + "active": { + "background": "#8b87922e", + "text": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14 + } + }, + "active_hover": { + "background": "#8b87921f", + "text": { + "family": "Zed Mono", + "color": "#19171c", + "size": 14 + } } }, - "hovered_entry": { - "height": 24, - "background": "#8b87921f", - "icon_color": "#585260", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#26232a", - "size": 14 - } - }, - "selected_entry": { - "height": 24, - "icon_color": "#585260", - "icon_size": 8, - "icon_spacing": 8, + "filename_editor": { + "background": "#e2dfe72e", "text": { "family": "Zed Mono", "color": "#26232a", "size": 14 - } - }, - "hovered_selected_entry": { - "height": 24, - "background": "#8b87921f", - "icon_color": "#585260", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#19171c", - "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" } } }, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 91b0e57ac2450859809467921baab4c0a4ac93d3..ba9b7189d35fada8abcc2b42bace6c9f5a81fd19 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -937,6 +937,7 @@ "top": 6, "bottom": 6 }, + "indent_width": 20, "entry": { "height": 24, "icon_color": "#555555", @@ -946,41 +947,42 @@ "family": "Zed Mono", "color": "#808080", "size": 14 + }, + "hover": { + "background": "#232323", + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + } + }, + "active": { + "background": "#2b2b2b", + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + } + }, + "active_hover": { + "background": "#232323", + "text": { + "family": "Zed Mono", + "color": "#ffffff", + "size": 14 + } } }, - "hovered_entry": { - "height": 24, - "background": "#232323", - "icon_color": "#555555", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#f1f1f1", - "size": 14 - } - }, - "selected_entry": { - "height": 24, - "icon_color": "#555555", - "icon_size": 8, - "icon_spacing": 8, + "filename_editor": { + "background": "#ffffff1f", "text": { "family": "Zed Mono", "color": "#f1f1f1", "size": 14 - } - }, - "hovered_selected_entry": { - "height": 24, - "background": "#232323", - "icon_color": "#555555", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#ffffff", - "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" } } }, diff --git a/assets/themes/light.json b/assets/themes/light.json index b66a8fe9ec7fc33f9aa5dc7d40bc20317c5b3918..7cbd315c8a41ff3572cb414727618607ae3e8875 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -937,6 +937,7 @@ "top": 6, "bottom": 6 }, + "indent_width": 20, "entry": { "height": 24, "icon_color": "#9c9c9c", @@ -946,41 +947,42 @@ "family": "Zed Mono", "color": "#636363", "size": 14 + }, + "hover": { + "background": "#eaeaea", + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + } + }, + "active": { + "background": "#e3e3e3", + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + } + }, + "active_hover": { + "background": "#eaeaea", + "text": { + "family": "Zed Mono", + "color": "#000000", + "size": 14 + } } }, - "hovered_entry": { - "height": 24, - "background": "#eaeaea", - "icon_color": "#9c9c9c", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#2b2b2b", - "size": 14 - } - }, - "selected_entry": { - "height": 24, - "icon_color": "#9c9c9c", - "icon_size": 8, - "icon_spacing": 8, + "filename_editor": { + "background": "#0000000f", "text": { "family": "Zed Mono", "color": "#2b2b2b", "size": 14 - } - }, - "hovered_selected_entry": { - "height": 24, - "background": "#eaeaea", - "icon_color": "#9c9c9c", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#000000", - "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" } } }, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 92d340a9b44a179742dc0972528a633bed58dfe2..8672518b4cf3ef8a93cc9ad4793a6ba0106c2faa 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -937,6 +937,7 @@ "top": 6, "bottom": 6 }, + "indent_width": 20, "entry": { "height": 24, "icon_color": "#93a1a1", @@ -946,41 +947,42 @@ "family": "Zed Mono", "color": "#93a1a1", "size": 14 + }, + "hover": { + "background": "#586e753d", + "text": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14 + } + }, + "active": { + "background": "#586e755c", + "text": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14 + } + }, + "active_hover": { + "background": "#586e753d", + "text": { + "family": "Zed Mono", + "color": "#fdf6e3", + "size": 14 + } } }, - "hovered_entry": { - "height": 24, - "background": "#586e753d", - "icon_color": "#93a1a1", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#eee8d5", - "size": 14 - } - }, - "selected_entry": { - "height": 24, - "icon_color": "#93a1a1", - "icon_size": 8, - "icon_spacing": 8, + "filename_editor": { + "background": "#0736425c", "text": { "family": "Zed Mono", "color": "#eee8d5", "size": 14 - } - }, - "hovered_selected_entry": { - "height": 24, - "background": "#586e753d", - "icon_color": "#93a1a1", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#fdf6e3", - "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" } } }, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index ee5acf2712fed50742083c72340cf3769f360e96..66b43e613dc725687d3f34c3ab427d7a1311ae61 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -937,6 +937,7 @@ "top": 6, "bottom": 6 }, + "indent_width": 20, "entry": { "height": 24, "icon_color": "#586e75", @@ -946,41 +947,42 @@ "family": "Zed Mono", "color": "#586e75", "size": 14 + }, + "hover": { + "background": "#93a1a11f", + "text": { + "family": "Zed Mono", + "color": "#073642", + "size": 14 + } + }, + "active": { + "background": "#93a1a12e", + "text": { + "family": "Zed Mono", + "color": "#073642", + "size": 14 + } + }, + "active_hover": { + "background": "#93a1a11f", + "text": { + "family": "Zed Mono", + "color": "#002b36", + "size": 14 + } } }, - "hovered_entry": { - "height": 24, - "background": "#93a1a11f", - "icon_color": "#586e75", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#073642", - "size": 14 - } - }, - "selected_entry": { - "height": 24, - "icon_color": "#586e75", - "icon_size": 8, - "icon_spacing": 8, + "filename_editor": { + "background": "#eee8d52e", "text": { "family": "Zed Mono", "color": "#073642", "size": 14 - } - }, - "hovered_selected_entry": { - "height": 24, - "background": "#93a1a11f", - "icon_color": "#586e75", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#002b36", - "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" } } }, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 7ae3c8870313fd5a192392199aea94f100ed8814..66f5182172e01e14902e28566600bbd7084de1de 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -937,6 +937,7 @@ "top": 6, "bottom": 6 }, + "indent_width": 20, "entry": { "height": 24, "icon_color": "#979db4", @@ -946,41 +947,42 @@ "family": "Zed Mono", "color": "#979db4", "size": 14 + }, + "hover": { + "background": "#5e66873d", + "text": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14 + } + }, + "active": { + "background": "#5e66875c", + "text": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14 + } + }, + "active_hover": { + "background": "#5e66873d", + "text": { + "family": "Zed Mono", + "color": "#f5f7ff", + "size": 14 + } } }, - "hovered_entry": { - "height": 24, - "background": "#5e66873d", - "icon_color": "#979db4", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#dfe2f1", - "size": 14 - } - }, - "selected_entry": { - "height": 24, - "icon_color": "#979db4", - "icon_size": 8, - "icon_spacing": 8, + "filename_editor": { + "background": "#2932565c", "text": { "family": "Zed Mono", "color": "#dfe2f1", "size": 14 - } - }, - "hovered_selected_entry": { - "height": 24, - "background": "#5e66873d", - "icon_color": "#979db4", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#f5f7ff", - "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" } } }, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 69eb194e3ce57d4a23c0bb60e491607b65d3cc04..34a33897288143e9e5c8492d5c753232ac1422b9 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -937,6 +937,7 @@ "top": 6, "bottom": 6 }, + "indent_width": 20, "entry": { "height": 24, "icon_color": "#5e6687", @@ -946,41 +947,42 @@ "family": "Zed Mono", "color": "#5e6687", "size": 14 + }, + "hover": { + "background": "#979db41f", + "text": { + "family": "Zed Mono", + "color": "#293256", + "size": 14 + } + }, + "active": { + "background": "#979db42e", + "text": { + "family": "Zed Mono", + "color": "#293256", + "size": 14 + } + }, + "active_hover": { + "background": "#979db41f", + "text": { + "family": "Zed Mono", + "color": "#202746", + "size": 14 + } } }, - "hovered_entry": { - "height": 24, - "background": "#979db41f", - "icon_color": "#5e6687", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#293256", - "size": 14 - } - }, - "selected_entry": { - "height": 24, - "icon_color": "#5e6687", - "icon_size": 8, - "icon_spacing": 8, + "filename_editor": { + "background": "#dfe2f12e", "text": { "family": "Zed Mono", "color": "#293256", "size": 14 - } - }, - "hovered_selected_entry": { - "height": 24, - "background": "#979db41f", - "icon_color": "#5e6687", - "icon_size": 8, - "icon_spacing": 8, - "text": { - "family": "Zed Mono", - "color": "#202746", - "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" } } }, diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index f722167b6f3696deaabcd671ea64d1c0fb908efe..499b3ed99d579af99ec02dddadad9a6aa77dd0d6 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -270,7 +270,7 @@ impl View for AutoUpdateIndicator { ) .boxed() }) - .on_click(|cx| cx.dispatch_action(DismissErrorMessage)) + .on_click(|_, cx| cx.dispatch_action(DismissErrorMessage)) .boxed() } AutoUpdateStatus::Idle => Empty::new().boxed(), diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 415ff6187ec72cca09c04cff901f34c9edadab27..bb835c66401d595607546fbca5453d85461c2164 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -320,7 +320,7 @@ impl ChatPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |cx| { + .on_click(move |_, cx| { let rpc = rpc.clone(); let this = this.clone(); cx.spawn(|mut cx| async move { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 1813d8f52d349fd57046fa6789b9730f549f269c..827ac564f827746700ea6456f35bd54289e64fe2 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -126,6 +126,9 @@ impl Server { .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::update_buffer) .add_message_handler(Server::update_buffer_file) .add_message_handler(Server::buffer_reloaded) @@ -157,9 +160,7 @@ impl Server { let span = info_span!( "handle message", payload_type = envelope.payload_type_name(), - payload = serde_json::to_string_pretty(&envelope.payload) - .unwrap() - .as_str(), + payload = format!("{:?}", envelope.payload).as_str(), ); let future = (handler)(server, *envelope); async move { @@ -447,6 +448,7 @@ impl Server { .cloned() .collect(), visible: worktree.visible, + scan_id: shared_worktree.scan_id, }) }) .collect(); @@ -577,6 +579,7 @@ impl Server { request.payload.worktree_id, &request.payload.removed_entries, &request.payload.updated_entries, + request.payload.scan_id, )?; broadcast(request.sender_id, connection_ids, |connection_id| { @@ -1808,6 +1811,176 @@ mod tests { .await; } + #[gpui::test(iterations = 10)] + async fn test_fs_operations( + executor: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + ) { + executor.forbid_parking(); + let fs = FakeFs::new(cx_a.background()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let mut client_a = server.create_client(cx_a, "user_a").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/dir", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + + let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await; + let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); + project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + let worktree_a = + project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); + let worktree_b = + project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); + + let entry = project_b + .update(cx_b, |project, cx| { + project + .create_entry((worktree_id, "c.txt"), false, cx) + .unwrap() + }) + .await + .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt", "c.txt"] + ); + }); + worktree_b.read_with(cx_b, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt", "c.txt"] + ); + }); + + project_b + .update(cx_b, |project, cx| { + project.rename_entry(entry.id, Path::new("d.txt"), cx) + }) + .unwrap() + .await + .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt", "d.txt"] + ); + }); + worktree_b.read_with(cx_b, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt", "d.txt"] + ); + }); + + let dir_entry = project_b + .update(cx_b, |project, cx| { + project + .create_entry((worktree_id, "DIR"), true, cx) + .unwrap() + }) + .await + .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"] + ); + }); + worktree_b.read_with(cx_b, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"] + ); + }); + + project_b + .update(cx_b, |project, cx| { + project.delete_entry(dir_entry.id, cx).unwrap() + }) + .await + .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt", "d.txt"] + ); + }); + worktree_b.read_with(cx_b, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt", "d.txt"] + ); + }); + + project_b + .update(cx_b, |project, cx| { + project.delete_entry(entry.id, cx).unwrap() + }) + .await + .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt"] + ); + }); + worktree_b.read_with(cx_b, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt"] + ); + }); + } + #[gpui::test(iterations = 10)] async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); @@ -3725,7 +3898,7 @@ mod tests { let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), cx) + workspace.open_path((worktree_id, "main.rs"), true, cx) }) .await .unwrap() @@ -3973,7 +4146,7 @@ mod tests { let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "one.rs"), cx) + workspace.open_path((worktree_id, "one.rs"), true, cx) }) .await .unwrap() @@ -4725,7 +4898,7 @@ mod tests { let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let editor_a1 = workspace_a .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), cx) + workspace.open_path((worktree_id, "1.txt"), true, cx) }) .await .unwrap() @@ -4733,7 +4906,7 @@ mod tests { .unwrap(); let editor_a2 = workspace_a .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), cx) + workspace.open_path((worktree_id, "2.txt"), true, cx) }) .await .unwrap() @@ -4744,7 +4917,7 @@ mod tests { let workspace_b = client_b.build_workspace(&project_b, cx_b); let editor_b1 = workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), cx) + workspace.open_path((worktree_id, "1.txt"), true, cx) }) .await .unwrap() @@ -4937,7 +5110,7 @@ mod tests { let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let _editor_a1 = workspace_a .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), cx) + workspace.open_path((worktree_id, "1.txt"), true, cx) }) .await .unwrap() @@ -4949,7 +5122,7 @@ mod tests { let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); let _editor_b1 = workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), cx) + workspace.open_path((worktree_id, "2.txt"), true, cx) }) .await .unwrap() @@ -4984,7 +5157,7 @@ mod tests { .update(cx_a, |workspace, cx| { workspace.activate_next_pane(cx); assert_eq!(*workspace.active_pane(), pane_a1); - workspace.open_path((worktree_id, "3.txt"), cx) + workspace.open_path((worktree_id, "3.txt"), true, cx) }) .await .unwrap(); @@ -4992,7 +5165,7 @@ mod tests { .update(cx_b, |workspace, cx| { workspace.activate_next_pane(cx); assert_eq!(*workspace.active_pane(), pane_b1); - workspace.open_path((worktree_id, "4.txt"), cx) + workspace.open_path((worktree_id, "4.txt"), true, cx) }) .await .unwrap(); @@ -5081,7 +5254,7 @@ mod tests { let workspace_a = client_a.build_workspace(&project_a, cx_a); let _editor_a1 = workspace_a .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), cx) + workspace.open_path((worktree_id, "1.txt"), true, cx) }) .await .unwrap() @@ -5194,7 +5367,7 @@ mod tests { // When client B activates a different item in the original pane, it automatically stops following client A. workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), cx) + workspace.open_path((worktree_id, "2.txt"), true, cx) }) .await .unwrap(); @@ -5633,6 +5806,7 @@ mod tests { guest_client.username, id ); + assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id()); } guest_client @@ -6354,6 +6528,49 @@ mod tests { client.buffers.extend(search.await?.into_keys()); } } + 60..=69 => { + let worktree = project + .read_with(cx, |project, cx| { + project + .worktrees(&cx) + .filter(|worktree| { + let worktree = worktree.read(cx); + worktree.is_visible() + && worktree.entries(false).any(|e| e.is_file()) + && worktree + .root_entry() + .map_or(false, |e| e.is_dir()) + }) + .choose(&mut *rng.lock()) + }) + .unwrap(); + let (worktree_id, worktree_root_name) = worktree + .read_with(cx, |worktree, _| { + (worktree.id(), worktree.root_name().to_string()) + }); + + let mut new_name = String::new(); + for _ in 0..10 { + let letter = rng.lock().gen_range('a'..='z'); + new_name.push(letter); + } + let mut new_path = PathBuf::new(); + new_path.push(new_name); + new_path.set_extension("rs"); + log::info!( + "{}: creating {:?} in worktree {} ({})", + guest_username, + new_path, + worktree_id, + worktree_root_name, + ); + project + .update(cx, |project, cx| { + project.create_entry((worktree_id, new_path), false, cx) + }) + .unwrap() + .await?; + } _ => { buffer.update(cx, |buffer, cx| { log::info!( diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 3be072c5e2e67b84b7cf5f70c7367f2e4b3be4d6..4737dd2c804ded463841948413d404d09d0858c0 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -46,6 +46,7 @@ pub struct ProjectShare { pub struct WorktreeShare { pub entries: HashMap, pub diagnostic_summaries: BTreeMap, + pub scan_id: u64, } #[derive(Default)] @@ -561,6 +562,7 @@ impl Store { worktree_id: u64, removed_entries: &[u64], updated_entries: &[proto::Entry], + scan_id: u64, ) -> Result> { let project = self.write_project(project_id, connection_id)?; let worktree = project @@ -574,6 +576,7 @@ impl Store { for entry in updated_entries { worktree.entries.insert(entry.id, entry.clone()); } + worktree.scan_id = scan_id; let connection_ids = project.connection_ids(); Ok(connection_ids) } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 45b5f69b5e2efea846b6ae49716777ea9ae0b7a1..171b4194960fc54f7c61b5d50e5e46d2a4b69c81 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -204,7 +204,7 @@ impl ContactsPanel { } else { CursorStyle::Arrow }) - .on_click(move |cx| { + .on_click(move |_, cx| { if !is_host && !is_guest { cx.dispatch_global_action(JoinProject { project_id, diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 39b5437a6a49360250e3ce28d8d4ae1885cfde67..ef99cbf5a87868d54c3c334c2957f8f6f2022d58 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -161,7 +161,7 @@ impl View for DiagnosticIndicator { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|cx| cx.dispatch_action(crate::Deploy)) + .on_click(|_, cx| cx.dispatch_action(crate::Deploy)) .aligned() .boxed(), ); @@ -194,7 +194,7 @@ impl View for DiagnosticIndicator { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|cx| cx.dispatch_action(GoToNextDiagnostic)) + .on_click(|_, cx| cx.dispatch_action(GoToNextDiagnostic)) .boxed(), ); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f47636c0932a1376422865ebec7ddcc0349a1228..440a5d53469f119c007cdf32e8b6a148121daaa5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9354,19 +9354,10 @@ mod tests { let fs = FakeFs::new(cx.background().clone()); fs.insert_file("/file.rs", Default::default()).await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/file.rs"], cx).await; project.update(cx, |project, _| project.languages().add(Arc::new(language))); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/file.rs", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .await .unwrap(); @@ -9485,19 +9476,10 @@ mod tests { let fs = FakeFs::new(cx.background().clone()); fs.insert_file("/file.rs", text).await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/file.rs"], cx).await; project.update(cx, |project, _| project.languages().add(Arc::new(language))); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/file.rs", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .await .unwrap(); let mut fake_server = fake_servers.next().await.unwrap(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 28ef9e5af04f2f7c5be3cb2338edf27c87facac1..a04d27a8bb669cc5427562069a9bed444421f591 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -875,6 +875,12 @@ impl Element for EditorElement { .max(constraint.min_along(Axis::Vertical)) .min(line_height * max_lines as f32), ) + } else if let EditorMode::SingleLine = snapshot.mode { + size.set_y( + line_height + .min(constraint.max_along(Axis::Vertical)) + .max(constraint.min_along(Axis::Vertical)), + ) } else if size.y().is_infinite() { size.set_y(scroll_height); } @@ -1183,7 +1189,7 @@ impl Element for EditorElement { click_count, .. } => self.mouse_down(*position, *alt, *shift, *click_count, layout, paint, cx), - Event::LeftMouseUp { position } => self.mouse_up(*position, cx), + Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx), Event::LeftMouseDragged { position } => { self.mouse_dragged(*position, layout, paint, cx) } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index ae176dccaa9c8ec7ba5dfbdafecebd6b1226a7d4..a63ff7b0bd18c6270963bbaf759940b1042cbdde 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -102,7 +102,7 @@ impl FileFinder { match event { Event::Selected(project_path) => { workspace - .open_path(project_path.clone(), cx) + .open_path(project_path.clone(), true, cx) .detach_and_log_err(cx); workspace.dismiss_modal(cx); } diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 12166d45b541b3d9316c372a464b2e121775b4ca..1ee7c6cbb5e57bf2108d36f48424fd3c9db7e058 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -15,7 +15,7 @@ pub struct MouseEventHandler { child: ElementBox, cursor_style: Option, mouse_down_handler: Option>, - click_handler: Option>, + click_handler: Option>, drag_handler: Option>, padding: Padding, } @@ -57,7 +57,7 @@ impl MouseEventHandler { self } - pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self { + pub fn on_click(mut self, handler: impl FnMut(usize, &mut EventContext) + 'static) -> Self { self.click_handler = Some(Box::new(handler)); self } @@ -151,14 +151,18 @@ impl Element for MouseEventHandler { handled_in_child } } - Event::LeftMouseUp { position, .. } => { + Event::LeftMouseUp { + position, + click_count, + .. + } => { state.prev_drag_position = None; if !handled_in_child && state.clicked { state.clicked = false; cx.notify(); if let Some(handler) = click_handler { if hit_bounds.contains_point(*position) { - handler(cx); + handler(*click_count, cx); } } true diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 2c80e01d6df6619395f587926cecd6c334eb6569..3126e18c7aef6caf6a51e9b97e263b23199e5f78 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -360,6 +360,14 @@ impl Deterministic { self.state.lock().now = new_now; } + + pub fn forbid_parking(&self) { + use rand::prelude::*; + + let mut state = self.state.lock(); + state.forbid_parking = true; + state.rng = StdRng::seed_from_u64(state.seed); + } } impl Drop for Timer { @@ -507,14 +515,8 @@ impl Foreground { #[cfg(any(test, feature = "test-support"))] pub fn forbid_parking(&self) { - use rand::prelude::*; - match self { - Self::Deterministic { executor, .. } => { - let mut state = executor.state.lock(); - state.forbid_parking = true; - state.rng = StdRng::seed_from_u64(state.seed); - } + Self::Deterministic { executor, .. } => executor.forbid_parking(), _ => panic!("this method can only be called on a deterministic executor"), } } diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index fe353fed4c9be36df5b74aa474be07d5123f3ad1..b32ab952c79dfbef1b1a539bec117463954c25bf 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -28,6 +28,7 @@ pub enum Event { }, LeftMouseUp { position: Vector2F, + click_count: usize, }, LeftMouseDragged { position: Vector2F, @@ -68,7 +69,7 @@ impl Event { Event::KeyDown { .. } => None, Event::ScrollWheel { position, .. } | Event::LeftMouseDown { position, .. } - | Event::LeftMouseUp { position } + | Event::LeftMouseUp { position, .. } | Event::LeftMouseDragged { position } | Event::RightMouseDown { position, .. } | Event::RightMouseUp { position } diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 7170bd2fd598d81301a3d10342f93c24040301d9..651805370c0be39bc17d5a0dde379f7c9e825a07 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -129,6 +129,7 @@ impl Event { native_event.locationInWindow().x as f32, window_height - native_event.locationInWindow().y as f32, ), + click_count: native_event.clickCount() as usize, }), NSEventType::NSRightMouseDown => { let modifiers = native_event.modifierFlags(); diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index 10cd0cd5a2934912665917e85237d2bb67b28575..d5d2105c3f34e6b74126080771118f07df8903af 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -119,7 +119,7 @@ impl View for Select { .with_style(style.header) .boxed() }) - .on_click(move |cx| cx.dispatch_action(ToggleSelect)) + .on_click(move |_, cx| cx.dispatch_action(ToggleSelect)) .boxed(), ); if self.is_open { @@ -153,7 +153,7 @@ impl View for Select { ) }, ) - .on_click(move |cx| cx.dispatch_action(SelectItem(ix))) + .on_click(move |_, cx| cx.dispatch_action(SelectItem(ix))) .boxed() })) }, diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 728dae312852480908315becf79e6c7ded7b9e99..eaae45bcc62d31f68adde4a8492c9d4f2e9a9f7b 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -29,7 +29,7 @@ settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } aho-corasick = "0.7" -anyhow = "1.0.38" +anyhow = "1.0.57" async-trait = "0.1" futures = "0.3" ignore = "0.4" diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index ec7925685d69173609d7344edad9521d363183ce..912dc65afeae289e4283a44f9d0a6d2bdc281956 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -379,7 +379,7 @@ impl FakeFs { async fn simulate_random_delay(&self) { self.executor .upgrade() - .expect("excecutor has been dropped") + .expect("executor has been dropped") .simulate_random_delay() .await; } @@ -493,7 +493,7 @@ impl Fs for FakeFs { }); for (relative_path, entry) in removed { - let new_path = target.join(relative_path); + let new_path = normalize_path(&target.join(relative_path)); state.entries.insert(new_path, entry); } @@ -501,13 +501,15 @@ impl Fs for FakeFs { Ok(()) } - async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { - let path = normalize_path(path); + async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> { + let dir_path = normalize_path(dir_path); let mut state = self.state.lock().await; - state.validate_path(&path)?; - if let Some(entry) = state.entries.get(&path) { + state.validate_path(&dir_path)?; + if let Some(entry) = state.entries.get(&dir_path) { if !entry.metadata.is_dir { - return Err(anyhow!("cannot remove {path:?} because it is not a dir")); + return Err(anyhow!( + "cannot remove {dir_path:?} because it is not a dir" + )); } if !options.recursive { @@ -517,14 +519,14 @@ impl Fs for FakeFs { .filter(|path| path.starts_with(path)) .count(); if descendants > 1 { - return Err(anyhow!("{path:?} is not empty")); + return Err(anyhow!("{dir_path:?} is not empty")); } } - state.entries.retain(|path, _| !path.starts_with(path)); - state.emit_event(&[path]).await; + state.entries.retain(|path, _| !path.starts_with(&dir_path)); + state.emit_event(&[dir_path]).await; } else if !options.ignore_if_not_exists { - return Err(anyhow!("{path:?} does not exist")); + return Err(anyhow!("{dir_path:?} does not exist")); } Ok(()) @@ -647,9 +649,16 @@ impl Fs for FakeFs { let (tx, rx) = smol::channel::unbounded(); state.event_txs.push(tx); let path = path.to_path_buf(); + let executor = self.executor.clone(); Box::pin(futures::StreamExt::filter(rx, move |events| { let result = events.iter().any(|event| event.path.starts_with(&path)); - async move { result } + let executor = executor.clone(); + async move { + if let Some(executor) = executor.clone().upgrade() { + executor.simulate_random_delay().await; + } + result + } })) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a9f24f2b6155cde6e3d8ca9e5b93de93165f1a73..1fcd89fcde90bee62b0a3831b4e2df3d491ed956 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -36,9 +36,11 @@ use std::{ cell::RefCell, cmp::{self, Ordering}, convert::TryInto, + ffi::OsString, hash::Hash, mem, ops::Range, + os::unix::{ffi::OsStrExt, prelude::OsStringExt}, path::{Component, Path, PathBuf}, rc::Rc, sync::{ @@ -225,6 +227,8 @@ impl DiagnosticSummary { pub struct ProjectEntryId(usize); impl ProjectEntryId { + pub const MAX: Self = Self(usize::MAX); + pub fn new(counter: &AtomicUsize) -> Self { Self(counter.fetch_add(1, SeqCst)) } @@ -257,6 +261,9 @@ impl Project { client.add_model_message_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_diagnostic_summary); client.add_model_message_handler(Self::handle_update_worktree); + client.add_model_request_handler(Self::handle_create_project_entry); + client.add_model_request_handler(Self::handle_rename_project_entry); + client.add_model_request_handler(Self::handle_delete_project_entry); client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_reload_buffers); @@ -452,12 +459,27 @@ impl Project { } #[cfg(any(test, feature = "test-support"))] - pub fn test(fs: Arc, cx: &mut gpui::TestAppContext) -> ModelHandle { + pub async fn test( + fs: Arc, + root_paths: impl IntoIterator>, + cx: &mut gpui::TestAppContext, + ) -> ModelHandle { let languages = Arc::new(LanguageRegistry::test()); let http_client = client::test::FakeHttpClient::with_404_response(); let client = client::Client::new(http_client.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - cx.update(|cx| Project::local(client, user_store, languages, fs, cx)) + let project = cx.update(|cx| Project::local(client, user_store, languages, fs, cx)); + for path in root_paths { + let (tree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) + }) + .await + .unwrap(); + tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + } + project } pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option> { @@ -669,6 +691,125 @@ impl Project { .map(|worktree| worktree.read(cx).id()) } + pub fn create_entry( + &mut self, + project_path: impl Into, + is_directory: bool, + cx: &mut ModelContext, + ) -> Option>> { + let project_path = project_path.into(); + let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + if self.is_local() { + Some(worktree.update(cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .create_entry(project_path.path, is_directory, cx) + })) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + Some(cx.spawn_weak(|_, mut cx| async move { + let response = client + .request(proto::CreateProjectEntry { + worktree_id: project_path.worktree_id.to_proto(), + project_id, + path: project_path.path.as_os_str().as_bytes().to_vec(), + is_directory, + }) + .await?; + let entry = response + .entry + .ok_or_else(|| anyhow!("missing entry in response"))?; + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + })) + } + } + + pub fn rename_entry( + &mut self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + let worktree = self.worktree_for_entry(entry_id, cx)?; + let new_path = new_path.into(); + if self.is_local() { + worktree.update(cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .rename_entry(entry_id, new_path, cx) + }) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + + Some(cx.spawn_weak(|_, mut cx| async move { + let response = client + .request(proto::RenameProjectEntry { + project_id, + entry_id: entry_id.to_proto(), + new_path: new_path.as_os_str().as_bytes().to_vec(), + }) + .await?; + let entry = response + .entry + .ok_or_else(|| anyhow!("missing entry in response"))?; + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + })) + } + } + + pub fn delete_entry( + &mut self, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Option>> { + let worktree = self.worktree_for_entry(entry_id, cx)?; + if self.is_local() { + worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().unwrap().delete_entry(entry_id, cx) + }) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + Some(cx.spawn_weak(|_, mut cx| async move { + let response = client + .request(proto::DeleteProjectEntry { + project_id, + entry_id: entry_id.to_proto(), + }) + .await?; + worktree + .update(&mut cx, move |worktree, cx| { + worktree.as_remote().unwrap().delete_entry( + entry_id, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + })) + } + } + pub fn can_share(&self, cx: &AppContext) -> bool { self.is_local() && self.visible_worktrees(cx).next().is_some() } @@ -850,6 +991,18 @@ impl Project { }) } + pub fn open_local_buffer( + &mut self, + abs_path: impl AsRef, + cx: &mut ModelContext, + ) -> Task>> { + if let Some((worktree, relative_path)) = self.find_local_worktree(abs_path.as_ref(), cx) { + self.open_buffer((worktree.read(cx).id(), relative_path), cx) + } else { + Task::ready(Err(anyhow!("no such path"))) + } + } + pub fn open_buffer( &mut self, path: impl Into, @@ -879,9 +1032,9 @@ impl Project { entry.insert(rx.clone()); let load_buffer = if worktree.read(cx).is_local() { - self.open_local_buffer(&project_path.path, &worktree, cx) + self.open_local_buffer_internal(&project_path.path, &worktree, cx) } else { - self.open_remote_buffer(&project_path.path, &worktree, cx) + self.open_remote_buffer_internal(&project_path.path, &worktree, cx) }; cx.spawn(move |this, mut cx| async move { @@ -911,7 +1064,7 @@ impl Project { }) } - fn open_local_buffer( + fn open_local_buffer_internal( &mut self, path: &Arc, worktree: &ModelHandle, @@ -928,7 +1081,7 @@ impl Project { }) } - fn open_remote_buffer( + fn open_remote_buffer_internal( &mut self, path: &Arc, worktree: &ModelHandle, @@ -3664,6 +3817,7 @@ impl Project { entries: Default::default(), diagnostic_summaries: Default::default(), visible: envelope.payload.visible, + scan_id: 0, }; let (worktree, load_task) = Worktree::remote(remote_id, replica_id, worktree, client, cx); @@ -3704,6 +3858,86 @@ impl Project { }) } + async fn handle_create_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let worktree = this.update(&mut cx, |this, cx| { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + this.worktree_for_id(worktree_id, cx) + .ok_or_else(|| anyhow!("worktree not found")) + })?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); + let entry = worktree + .update(&mut cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + let path = PathBuf::from(OsString::from_vec(envelope.payload.path)); + worktree.create_entry(path, envelope.payload.is_directory, cx) + }) + .await?; + Ok(proto::ProjectEntryResponse { + entry: Some((&entry).into()), + worktree_scan_id: worktree_scan_id as u64, + }) + } + + async fn handle_rename_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + let worktree = this.read_with(&cx, |this, cx| { + this.worktree_for_entry(entry_id, cx) + .ok_or_else(|| anyhow!("worktree not found")) + })?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); + let entry = worktree + .update(&mut cx, |worktree, cx| { + let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path)); + worktree + .as_local_mut() + .unwrap() + .rename_entry(entry_id, new_path, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + Ok(proto::ProjectEntryResponse { + entry: Some((&entry).into()), + worktree_scan_id: worktree_scan_id as u64, + }) + } + + async fn handle_delete_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + let worktree = this.read_with(&cx, |this, cx| { + this.worktree_for_entry(entry_id, cx) + .ok_or_else(|| anyhow!("worktree not found")) + })?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); + worktree + .update(&mut cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .delete_entry(entry_id, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + Ok(proto::ProjectEntryResponse { + entry: None, + worktree_scan_id: worktree_scan_id as u64, + }) + } + async fn handle_update_diagnostic_summary( this: ModelHandle, envelope: TypedEnvelope, @@ -4905,6 +5139,8 @@ impl Item for Buffer { #[cfg(test)] mod tests { + use crate::worktree::WorktreeHandle; + use super::{Event, *}; use fs::RealFs; use futures::{future, StreamExt}; @@ -4918,7 +5154,6 @@ mod tests { use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc, task::Poll}; use unindent::Unindent as _; use util::{assert_set_eq, test::temp_tree}; - use worktree::WorktreeHandle as _; #[gpui::test] async fn test_populate_and_search(cx: &mut gpui::TestAppContext) { @@ -4945,19 +5180,10 @@ mod tests { ) .unwrap(); - let project = Project::test(Arc::new(RealFs), cx); + let project = Project::test(Arc::new(RealFs), [root_link_path], cx).await; - let (tree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree(&root_link_path, true, cx) - }) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - cx.read(|cx| { - let tree = tree.read(cx); + project.read_with(cx, |project, cx| { + let tree = project.worktrees(cx).next().unwrap().read(cx); assert_eq!(tree.file_count(), 5); assert_eq!( tree.inode_for_path("fennel/grape"), @@ -5038,25 +5264,16 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), cx); + let project = Project::test(fs.clone(), ["/the-root"], cx).await; project.update(cx, |project, _| { project.languages.add(Arc::new(rust_language)); project.languages.add(Arc::new(json_language)); }); - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/the-root", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); - // Open a buffer without an associated language server. let toml_buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "Cargo.toml"), cx) + project.open_local_buffer("/the-root/Cargo.toml", cx) }) .await .unwrap(); @@ -5064,7 +5281,7 @@ mod tests { // Open a buffer with an associated language server. let rust_buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "test.rs"), cx) + project.open_local_buffer("/the-root/test.rs", cx) }) .await .unwrap(); @@ -5111,7 +5328,7 @@ mod tests { // Open a third buffer with a different associated language server. let json_buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "package.json"), cx) + project.open_local_buffer("/the-root/package.json", cx) }) .await .unwrap(); @@ -5141,7 +5358,7 @@ mod tests { // it is also configured based on the existing language server's capabilities. let rust_buffer2 = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "test2.rs"), cx) + project.open_local_buffer("/the-root/test2.rs", cx) }) .await .unwrap(); @@ -5270,6 +5487,7 @@ mod tests { language_id: Default::default() }, ); + // We clear the diagnostics, since the language has changed. rust_buffer2.read_with(cx, |buffer, _| { assert_eq!( @@ -5382,34 +5600,14 @@ mod tests { ) .await; - let project = Project::test(fs, cx); - let worktree_a_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir/a.rs", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); - let worktree_b_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir/b.rs", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); + let project = Project::test(fs, ["/dir/a.rs", "/dir/b.rs"], cx).await; let buffer_a = project - .update(cx, |project, cx| { - project.open_buffer((worktree_a_id, ""), cx) - }) + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) .await .unwrap(); let buffer_b = project - .update(cx, |project, cx| { - project.open_buffer((worktree_b_id, ""), cx) - }) + .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) .await .unwrap(); @@ -5513,25 +5711,14 @@ mod tests { ) .await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/dir"], cx).await; project.update(cx, |project, _| project.languages.add(Arc::new(language))); - - let (tree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap(); - let worktree_id = tree.read_with(cx, |tree, _| tree.id()); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; + let worktree_id = + project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id()); // Cause worktree to start the fake language server let _buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, Path::new("b.rs")), cx) - }) + .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) .await .unwrap(); @@ -5577,7 +5764,7 @@ mod tests { ); let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx)) .await .unwrap(); @@ -5646,22 +5833,11 @@ mod tests { let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({ "a.rs": "" })).await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/dir"], cx).await; project.update(cx, |project, _| project.languages.add(Arc::new(language))); - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, "a.rs"), cx) - }) + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) .await .unwrap(); @@ -5726,22 +5902,11 @@ mod tests { let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({ "a.rs": text })).await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/dir"], cx).await; project.update(cx, |project, _| project.languages.add(Arc::new(language))); - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, "a.rs"), cx) - }) + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) .await .unwrap(); @@ -6006,20 +6171,9 @@ mod tests { let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({ "a.rs": text })).await; - let project = Project::test(fs, cx); - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); - + let project = Project::test(fs, ["/dir"], cx).await; let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, "a.rs"), cx) - }) + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) .await .unwrap(); @@ -6108,22 +6262,10 @@ mod tests { ) .await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/dir"], cx).await; project.update(cx, |project, _| project.languages.add(Arc::new(language))); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, "a.rs"), cx) - }) + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) .await .unwrap(); @@ -6274,20 +6416,9 @@ mod tests { ) .await; - let project = Project::test(fs, cx); - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); - + let project = Project::test(fs, ["/dir"], cx).await; let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, "a.rs"), cx) - }) + .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) .await .unwrap(); @@ -6408,17 +6539,7 @@ mod tests { } })); - let project = Project::test(Arc::new(RealFs), cx); - let (tree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree(&dir.path(), true, cx) - }) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - + let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await; let cancel_flag = Default::default(); let results = project .read_with(cx, |project, cx| { @@ -6451,21 +6572,11 @@ mod tests { ) .await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/dir/b.rs"], cx).await; project.update(cx, |project, _| project.languages.add(Arc::new(language))); - let (tree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir/b.rs", true, cx) - }) - .await - .unwrap(); - let worktree_id = tree.read_with(cx, |tree, _| tree.id()); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - let buffer = project - .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) .await .unwrap(); @@ -6555,21 +6666,10 @@ mod tests { ) .await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/dir"], cx).await; project.update(cx, |project, _| project.languages.add(Arc::new(language))); - - let (tree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap(); - let worktree_id = tree.read_with(cx, |tree, _| tree.id()); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) .await .unwrap(); @@ -6624,21 +6724,10 @@ mod tests { ) .await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/dir"], cx).await; project.update(cx, |project, _| project.languages.add(Arc::new(language))); - - let (tree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap(); - let worktree_id = tree.read_with(cx, |tree, _| tree.id()); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) .await .unwrap(); @@ -6741,18 +6830,9 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), cx); - let worktree_id = project - .update(cx, |p, cx| { - p.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); - + let project = Project::test(fs.clone(), ["/dir"], cx).await; let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) .await .unwrap(); buffer @@ -6779,18 +6859,9 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), cx); - let worktree_id = project - .update(cx, |p, cx| { - p.find_or_create_local_worktree("/dir/file1", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); - + let project = Project::test(fs.clone(), ["/dir/file1"], cx).await; let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, ""), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) .await .unwrap(); buffer @@ -6810,15 +6881,7 @@ mod tests { let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({})).await; - let project = Project::test(fs.clone(), cx); - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap(); - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - + let project = Project::test(fs.clone(), ["/dir"], cx).await; let buffer = project.update(cx, |project, cx| { project.create_buffer("", None, cx).unwrap() }); @@ -6842,7 +6905,7 @@ mod tests { let opened_buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "file1"), cx) + project.open_local_buffer("/dir/file1", cx) }) .await .unwrap(); @@ -6865,24 +6928,18 @@ mod tests { } })); - let project = Project::test(Arc::new(RealFs), cx); + let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await; let rpc = project.read_with(cx, |p, _| p.client.clone()); - let (tree, _) = project - .update(cx, |p, cx| { - p.find_or_create_local_worktree(dir.path(), true, cx) - }) - .await - .unwrap(); - let worktree_id = tree.read_with(cx, |tree, _| tree.id()); - let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { - let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, path), cx)); + let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx)); async move { buffer.await.unwrap() } }; let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| { - tree.read_with(cx, |tree, _| { - tree.entry_for_path(path) + project.read_with(cx, |project, cx| { + let tree = project.worktrees(cx).next().unwrap(); + tree.read(cx) + .entry_for_path(path) .expect(&format!("no entry for path {}", path)) .id }) @@ -6897,11 +6954,8 @@ mod tests { let file3_id = id_for_path("a/file3", &cx); let file4_id = id_for_path("b/c/file4", &cx); - // Wait for the initial scan. - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - // Create a remote copy of this worktree. + let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); let (remote, load_task) = cx.update(|cx| { Worktree::remote( @@ -6912,6 +6966,7 @@ mod tests { cx, ) }); + // tree load_task.await; cx.read(|cx| { @@ -7005,7 +7060,7 @@ mod tests { async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.background()); fs.insert_tree( - "/the-dir", + "/dir", json!({ "a.txt": "a-contents", "b.txt": "b-contents", @@ -7013,22 +7068,14 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), cx); - let worktree_id = project - .update(cx, |p, cx| { - p.find_or_create_local_worktree("/the-dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); + let project = Project::test(fs.clone(), ["/dir"], cx).await; // Spawn multiple tasks to open paths, repeating some paths. let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| { ( - p.open_buffer((worktree_id, "a.txt"), cx), - p.open_buffer((worktree_id, "b.txt"), cx), - p.open_buffer((worktree_id, "a.txt"), cx), + p.open_local_buffer("/dir/a.txt", cx), + p.open_local_buffer("/dir/b.txt", cx), + p.open_local_buffer("/dir/a.txt", cx), ) }); @@ -7045,7 +7092,7 @@ mod tests { // Open the same path again while it is still open. drop(buffer_a_1); let buffer_a_3 = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx)) .await .unwrap(); @@ -7055,30 +7102,21 @@ mod tests { #[gpui::test] async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { - use std::fs; - - let dir = temp_tree(json!({ - "file1": "abc", - "file2": "def", - "file3": "ghi", - })); - - let project = Project::test(Arc::new(RealFs), cx); - let (worktree, _) = project - .update(cx, |p, cx| { - p.find_or_create_local_worktree(dir.path(), true, cx) - }) - .await - .unwrap(); - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "file1": "abc", + "file2": "def", + "file3": "ghi", + }), + ) + .await; - worktree.flush_fs_events(&cx).await; - worktree - .read_with(cx, |t, _| t.as_local().unwrap().scan_complete()) - .await; + let project = Project::test(fs.clone(), ["/dir"], cx).await; let buffer1 = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) .await .unwrap(); let events = Rc::new(RefCell::new(Vec::new())); @@ -7148,7 +7186,7 @@ mod tests { // When a file is deleted, the buffer is considered dirty. let events = Rc::new(RefCell::new(Vec::new())); let buffer2 = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "file2"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx)) .await .unwrap(); buffer2.update(cx, |_, cx| { @@ -7159,7 +7197,9 @@ mod tests { .detach(); }); - fs::remove_file(dir.path().join("file2")).unwrap(); + fs.remove_file("/dir/file2".as_ref(), Default::default()) + .await + .unwrap(); buffer2.condition(&cx, |b, _| b.is_dirty()).await; assert_eq!( *events.borrow(), @@ -7169,7 +7209,7 @@ mod tests { // When a file is already dirty when deleted, we don't emit a Dirtied event. let events = Rc::new(RefCell::new(Vec::new())); let buffer3 = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "file3"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx)) .await .unwrap(); buffer3.update(cx, |_, cx| { @@ -7180,12 +7220,13 @@ mod tests { .detach(); }); - worktree.flush_fs_events(&cx).await; buffer3.update(cx, |buffer, cx| { buffer.edit([(0..0, "x")], cx); }); events.borrow_mut().clear(); - fs::remove_file(dir.path().join("file3")).unwrap(); + fs.remove_file("/dir/file3".as_ref(), Default::default()) + .await + .unwrap(); buffer3 .condition(&cx, |_, _| !events.borrow().is_empty()) .await; @@ -7195,47 +7236,24 @@ mod tests { #[gpui::test] async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { - use std::fs; - let initial_contents = "aaa\nbbbbb\nc\n"; - let dir = temp_tree(json!({ "the-file": initial_contents })); - - let project = Project::test(Arc::new(RealFs), cx); - let (worktree, _) = project - .update(cx, |p, cx| { - p.find_or_create_local_worktree(dir.path(), true, cx) - }) - .await - .unwrap(); - let worktree_id = worktree.read_with(cx, |tree, _| tree.id()); - - worktree - .read_with(cx, |t, _| t.as_local().unwrap().scan_complete()) - .await; - - let abs_path = dir.path().join("the-file"); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "the-file": initial_contents, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir"], cx).await; let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "the-file"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx)) .await .unwrap(); - // TODO - // Add a cursor on each row. - // let selection_set_id = buffer.update(&mut cx, |buffer, cx| { - // assert!(!buffer.is_dirty()); - // buffer.add_selection_set( - // &(0..3) - // .map(|row| Selection { - // id: row as usize, - // start: Point::new(row, 1), - // end: Point::new(row, 1), - // reversed: false, - // goal: SelectionGoal::None, - // }) - // .collect::>(), - // cx, - // ) - // }); + let anchors = (0..3) + .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1)))) + .collect::>(); // Change the file on disk, adding two new lines of text, and removing // one line. @@ -7244,7 +7262,9 @@ mod tests { assert!(!buffer.has_conflict()); }); let new_contents = "AAAA\naaa\nBB\nbbbbb\n"; - fs::write(&abs_path, new_contents).unwrap(); + fs.save("/dir/the-file".as_ref(), &new_contents.into()) + .await + .unwrap(); // Because the buffer was not modified, it is reloaded from disk. Its // contents are edited according to the diff between the old and new @@ -7258,20 +7278,14 @@ mod tests { assert!(!buffer.is_dirty()); assert!(!buffer.has_conflict()); - // TODO - // let cursor_positions = buffer - // .selection_set(selection_set_id) - // .unwrap() - // .selections::(&*buffer) - // .map(|selection| { - // assert_eq!(selection.start, selection.end); - // selection.start - // }) - // .collect::>(); - // assert_eq!( - // cursor_positions, - // [Point::new(1, 1), Point::new(3, 1), Point::new(4, 0)] - // ); + let anchor_positions = anchors + .iter() + .map(|anchor| anchor.to_point(&*buffer)) + .collect::>(); + assert_eq!( + anchor_positions, + [Point::new(1, 1), Point::new(3, 1), Point::new(4, 0)] + ); }); // Modify the buffer @@ -7282,7 +7296,12 @@ mod tests { }); // Change the file on disk again, adding blank lines to the beginning. - fs::write(&abs_path, "\n\n\nAAAA\naaa\nBB\nbbbbb\n").unwrap(); + fs.save( + "/dir/the-file".as_ref(), + &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(), + ) + .await + .unwrap(); // Because the buffer is modified, it doesn't reload from disk, but is // marked as having a conflict. @@ -7311,17 +7330,9 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), cx); - let (worktree, _) = project - .update(cx, |p, cx| { - p.find_or_create_local_worktree("/the-dir", true, cx) - }) - .await - .unwrap(); - let worktree_id = worktree.read_with(cx, |tree, _| tree.id()); - + let project = Project::test(fs.clone(), ["/the-dir"], cx).await; let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx)) .await .unwrap(); @@ -7583,22 +7594,11 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), cx); + let project = Project::test(fs.clone(), ["/dir"], cx).await; project.update(cx, |project, _| project.languages.add(Arc::new(language))); - - let (tree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap(); - let worktree_id = tree.read_with(cx, |tree, _| tree.id()); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - let buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, Path::new("one.rs")), cx) + project.open_local_buffer("/dir/one.rs", cx) }) .await .unwrap(); @@ -7713,17 +7713,7 @@ mod tests { }), ) .await; - let project = Project::test(fs.clone(), cx); - let (tree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap(); - let worktree_id = tree.read_with(cx, |tree, _| tree.id()); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - + let project = Project::test(fs.clone(), ["/dir"], cx).await; assert_eq!( search(&project, SearchQuery::text("TWO", false, true), cx) .await @@ -7736,7 +7726,7 @@ mod tests { let buffer_4 = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "four.rs"), cx) + project.open_local_buffer("/dir/four.rs", cx) }) .await .unwrap(); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 19b490b0e294a302f8122b43444d819561830422..bab41bbe278e21330b1f695a09feb9375efc105f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1,4 +1,4 @@ -use crate::ProjectEntryId; +use crate::{ProjectEntryId, RemoveOptions}; use super::{ fs::{self, Fs}, @@ -42,6 +42,7 @@ use std::{ fmt, future::Future, ops::{Deref, DerefMut}, + os::unix::prelude::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, sync::{atomic::AtomicUsize, Arc}, time::{Duration, SystemTime}, @@ -78,11 +79,12 @@ pub struct LocalWorktree { } pub struct RemoteWorktree { - pub(crate) snapshot: Snapshot, + pub snapshot: Snapshot, + pub(crate) background_snapshot: Arc>, project_id: u64, - snapshot_rx: watch::Receiver, client: Arc, updates_tx: UnboundedSender, + last_scan_id_rx: watch::Receiver, replica_id: ReplicaId, diagnostic_summaries: TreeMap, visible: bool, @@ -95,12 +97,12 @@ pub struct Snapshot { root_char_bag: CharBag, entries_by_path: SumTree, entries_by_id: SumTree, + scan_id: usize, } #[derive(Clone)] pub struct LocalSnapshot { abs_path: Arc, - scan_id: usize, ignores: HashMap, (Arc, usize)>, removed_entry_ids: HashMap, next_entry_id: Arc, @@ -214,17 +216,21 @@ impl Worktree { root_char_bag, entries_by_path: Default::default(), entries_by_id: Default::default(), + scan_id: worktree.scan_id as usize, }; let (updates_tx, mut updates_rx) = mpsc::unbounded(); - let (mut snapshot_tx, snapshot_rx) = watch::channel_with(snapshot.clone()); + let background_snapshot = Arc::new(Mutex::new(snapshot.clone())); + let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); + let (mut last_scan_id_tx, last_scan_id_rx) = watch::channel_with(worktree.scan_id as usize); let worktree_handle = cx.add_model(|_: &mut ModelContext| { Worktree::Remote(RemoteWorktree { project_id: project_remote_id, replica_id, snapshot: snapshot.clone(), - snapshot_rx: snapshot_rx.clone(), + background_snapshot: background_snapshot.clone(), updates_tx, + last_scan_id_rx, client: client.clone(), diagnostic_summaries: TreeMap::from_ordered_entries( worktree.diagnostic_summaries.into_iter().map(|summary| { @@ -274,37 +280,42 @@ impl Worktree { .await; { - let mut snapshot = snapshot_tx.borrow_mut(); + let mut snapshot = background_snapshot.lock(); snapshot.entries_by_path = entries_by_path; snapshot.entries_by_id = entries_by_id; + snapshot_updated_tx.send(()).await.ok(); } cx.background() .spawn(async move { while let Some(update) = updates_rx.next().await { - let mut snapshot = snapshot_tx.borrow().clone(); - if let Err(error) = snapshot.apply_remote_update(update) { + if let Err(error) = + background_snapshot.lock().apply_remote_update(update) + { log::error!("error applying worktree update: {}", error); } - *snapshot_tx.borrow_mut() = snapshot; + snapshot_updated_tx.send(()).await.ok(); } }) .detach(); - { - let mut snapshot_rx = snapshot_rx.clone(); + cx.spawn(|mut cx| { let this = worktree_handle.downgrade(); - cx.spawn(|mut cx| async move { - while let Some(_) = snapshot_rx.recv().await { + async move { + while let Some(_) = snapshot_updated_rx.recv().await { if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + let this = this.as_remote_mut().unwrap(); + *last_scan_id_tx.borrow_mut() = this.snapshot.scan_id; + }); } else { break; } } - }) - .detach(); - } + } + }) + .detach(); } }); (worktree_handle, deserialize_task) @@ -357,6 +368,13 @@ impl Worktree { } } + pub fn scan_id(&self) -> usize { + match self { + Worktree::Local(worktree) => worktree.snapshot.scan_id, + Worktree::Remote(worktree) => worktree.snapshot.scan_id, + } + } + pub fn is_visible(&self) -> bool { match self { Worktree::Local(worktree) => worktree.visible, @@ -410,7 +428,7 @@ impl Worktree { } } Self::Remote(worktree) => { - worktree.snapshot = worktree.snapshot_rx.borrow().clone(); + worktree.snapshot = worktree.background_snapshot.lock().clone(); cx.emit(Event::UpdatedEntries); } }; @@ -454,7 +472,6 @@ impl LocalWorktree { let tree = cx.add_model(move |cx: &mut ModelContext| { let mut snapshot = LocalSnapshot { abs_path, - scan_id: 0, ignores: Default::default(), removed_entry_ids: Default::default(), next_entry_id, @@ -464,6 +481,7 @@ impl LocalWorktree { root_char_bag, entries_by_path: Default::default(), entries_by_id: Default::default(), + scan_id: 0, }, }; if let Some(metadata) = metadata { @@ -494,24 +512,13 @@ impl LocalWorktree { cx.spawn_weak(|this, mut cx| async move { while let Some(scan_state) = scan_states_rx.next().await { - if let Some(handle) = this.upgrade(&cx) { - let to_send = handle.update(&mut cx, |this, cx| { - last_scan_state_tx.blocking_send(scan_state).ok(); + if let Some(this) = this.upgrade(&cx) { + last_scan_state_tx.blocking_send(scan_state).ok(); + this.update(&mut cx, |this, cx| { this.poll_snapshot(cx); - let tree = this.as_local_mut().unwrap(); - if !tree.is_scanning() { - if let Some(share) = tree.share.as_ref() { - return Some((tree.snapshot(), share.snapshots_tx.clone())); - } - } - None - }); - - if let Some((snapshot, snapshots_to_send_tx)) = to_send { - if let Err(err) = snapshots_to_send_tx.send(snapshot).await { - log::error!("error submitting snapshot to send {}", err); - } - } + this.as_local().unwrap().broadcast_snapshot() + }) + .await; } else { break; } @@ -623,12 +630,15 @@ impl LocalWorktree { let handle = cx.handle(); let path = Arc::from(path); let abs_path = self.absolutize(&path); - let background_snapshot = self.background_snapshot.clone(); let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; // Eagerly populate the snapshot with an updated entry for the loaded file - let entry = refresh_entry(fs.as_ref(), &background_snapshot, path, &abs_path).await?; + let entry = this + .update(&mut cx, |this, _| { + this.as_local().unwrap().refresh_entry(path, abs_path, None) + }) + .await?; this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); Ok(( File { @@ -652,7 +662,7 @@ impl LocalWorktree { let buffer = buffer_handle.read(cx); let text = buffer.as_rope().clone(); let version = buffer.version(); - let save = self.save(path, text, cx); + let save = self.write_file(path, text, cx); let handle = cx.handle(); cx.as_mut().spawn(|mut cx| async move { let entry = save.await?; @@ -672,28 +682,188 @@ impl LocalWorktree { }) } - fn save( + pub fn create_entry( + &self, + path: impl Into>, + is_dir: bool, + cx: &mut ModelContext, + ) -> Task> { + self.write_entry_internal( + path, + if is_dir { + None + } else { + Some(Default::default()) + }, + cx, + ) + } + + pub fn write_file( &self, path: impl Into>, text: Rope, cx: &mut ModelContext, + ) -> Task> { + self.write_entry_internal(path, Some(text), cx) + } + + pub fn delete_entry( + &self, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Option>> { + let entry = self.entry_for_id(entry_id)?.clone(); + let abs_path = self.absolutize(&entry.path); + let delete = cx.background().spawn({ + let fs = self.fs.clone(); + let abs_path = abs_path.clone(); + async move { + if entry.is_file() { + fs.remove_file(&abs_path, Default::default()).await + } else { + fs.remove_dir( + &abs_path, + RemoveOptions { + recursive: true, + ignore_if_not_exists: false, + }, + ) + .await + } + } + }); + + Some(cx.spawn(|this, mut cx| async move { + delete.await?; + this.update(&mut cx, |this, _| { + let this = this.as_local_mut().unwrap(); + let mut snapshot = this.background_snapshot.lock(); + snapshot.delete_entry(entry_id); + }); + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + this.as_local().unwrap().broadcast_snapshot() + }) + .await; + Ok(()) + })) + } + + pub fn rename_entry( + &self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + let old_path = self.entry_for_id(entry_id)?.path.clone(); + let new_path = new_path.into(); + let abs_old_path = self.absolutize(&old_path); + let abs_new_path = self.absolutize(&new_path); + let rename = cx.background().spawn({ + let fs = self.fs.clone(); + let abs_new_path = abs_new_path.clone(); + async move { + fs.rename(&abs_old_path, &abs_new_path, Default::default()) + .await + } + }); + + Some(cx.spawn(|this, mut cx| async move { + rename.await?; + let entry = this + .update(&mut cx, |this, _| { + this.as_local_mut().unwrap().refresh_entry( + new_path.clone(), + abs_new_path, + Some(old_path), + ) + }) + .await?; + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + this.as_local().unwrap().broadcast_snapshot() + }) + .await; + Ok(entry) + })) + } + + fn write_entry_internal( + &self, + path: impl Into>, + text_if_file: Option, + cx: &mut ModelContext, ) -> Task> { let path = path.into(); let abs_path = self.absolutize(&path); - let background_snapshot = self.background_snapshot.clone(); - let fs = self.fs.clone(); - let save = cx.background().spawn(async move { - fs.save(&abs_path, &text).await?; - refresh_entry(fs.as_ref(), &background_snapshot, path.clone(), &abs_path).await + let write = cx.background().spawn({ + let fs = self.fs.clone(); + let abs_path = abs_path.clone(); + async move { + if let Some(text) = text_if_file { + fs.save(&abs_path, &text).await + } else { + fs.create_dir(&abs_path).await + } + } }); cx.spawn(|this, mut cx| async move { - let entry = save.await?; - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + write.await?; + let entry = this + .update(&mut cx, |this, _| { + this.as_local_mut() + .unwrap() + .refresh_entry(path, abs_path, None) + }) + .await?; + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + this.as_local().unwrap().broadcast_snapshot() + }) + .await; Ok(entry) }) } + fn refresh_entry( + &self, + path: Arc, + abs_path: PathBuf, + old_path: Option>, + ) -> impl Future> { + let root_char_bag; + let next_entry_id; + let fs = self.fs.clone(); + let shared_snapshots_tx = self.share.as_ref().map(|share| share.snapshots_tx.clone()); + let snapshot = self.background_snapshot.clone(); + { + let snapshot = snapshot.lock(); + root_char_bag = snapshot.root_char_bag; + next_entry_id = snapshot.next_entry_id.clone(); + } + async move { + let entry = Entry::new( + path, + &fs.metadata(&abs_path) + .await? + .ok_or_else(|| anyhow!("could not read saved file metadata"))?, + &next_entry_id, + root_char_bag, + ); + let mut snapshot = snapshot.lock(); + if let Some(old_path) = old_path { + snapshot.remove_path(&old_path); + } + let entry = snapshot.insert_entry(entry, fs.as_ref()); + if let Some(tx) = shared_snapshots_tx { + tx.send(snapshot.clone()).await.ok(); + } + Ok(entry) + } + } + pub fn register( &mut self, project_id: u64, @@ -761,6 +931,7 @@ impl LocalWorktree { .map(Into::into) .collect(), removed_entries: Default::default(), + scan_id: snapshot.scan_id as u64, }) .await { @@ -829,6 +1000,23 @@ impl LocalWorktree { pub fn is_shared(&self) -> bool { self.share.is_some() } + + fn broadcast_snapshot(&self) -> impl Future { + let mut to_send = None; + if !self.is_scanning() { + if let Some(share) = self.share.as_ref() { + to_send = Some((self.snapshot(), share.snapshots_tx.clone())); + } + } + + async move { + if let Some((snapshot, snapshots_to_send_tx)) = to_send { + if let Err(err) = snapshots_to_send_tx.send(snapshot).await { + log::error!("error submitting snapshot to send {}", err); + } + } + } + } } impl RemoteWorktree { @@ -843,10 +1031,20 @@ impl RemoteWorktree { self.updates_tx .unbounded_send(envelope.payload) .expect("consumer runs to completion"); - Ok(()) } + fn wait_for_snapshot(&self, scan_id: usize) -> impl Future { + let mut rx = self.last_scan_id_rx.clone(); + async move { + while let Some(applied_scan_id) = rx.next().await { + if applied_scan_id >= scan_id { + return; + } + } + } + } + pub fn update_diagnostic_summary( &mut self, path: Arc, @@ -863,6 +1061,44 @@ impl RemoteWorktree { .insert(PathKey(path.clone()), summary); } } + + pub fn insert_entry( + &self, + entry: proto::Entry, + scan_id: usize, + cx: &mut ModelContext, + ) -> Task> { + let wait_for_snapshot = self.wait_for_snapshot(scan_id); + cx.spawn(|this, mut cx| async move { + wait_for_snapshot.await; + this.update(&mut cx, |worktree, _| { + let worktree = worktree.as_remote_mut().unwrap(); + let mut snapshot = worktree.background_snapshot.lock(); + let entry = snapshot.insert_entry(entry); + worktree.snapshot = snapshot.clone(); + entry + }) + }) + } + + pub(crate) fn delete_entry( + &self, + id: ProjectEntryId, + scan_id: usize, + cx: &mut ModelContext, + ) -> Task> { + let wait_for_snapshot = self.wait_for_snapshot(scan_id); + cx.spawn(|this, mut cx| async move { + wait_for_snapshot.await; + this.update(&mut cx, |worktree, _| { + let worktree = worktree.as_remote_mut().unwrap(); + let mut snapshot = worktree.background_snapshot.lock(); + snapshot.delete_entry(id); + worktree.snapshot = snapshot.clone(); + }); + Ok(()) + }) + } } impl Snapshot { @@ -874,6 +1110,33 @@ impl Snapshot { self.entries_by_id.get(&entry_id, &()).is_some() } + pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result { + let entry = Entry::try_from((&self.root_char_bag, entry))?; + let old_entry = self.entries_by_id.insert_or_replace( + PathEntry { + id: entry.id, + path: entry.path.clone(), + is_ignored: entry.is_ignored, + scan_id: 0, + }, + &(), + ); + if let Some(old_entry) = old_entry { + self.entries_by_path.remove(&PathKey(old_entry.path), &()); + } + self.entries_by_path.insert_or_replace(entry.clone(), &()); + Ok(entry) + } + + fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool { + if let Some(entry) = self.entries_by_id.remove(&entry_id, &()) { + self.entries_by_path.remove(&PathKey(entry.path), &()); + true + } else { + false + } + } + pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> { let mut entries_by_path_edits = Vec::new(); let mut entries_by_id_edits = Vec::new(); @@ -901,6 +1164,7 @@ impl Snapshot { self.entries_by_path.edit(entries_by_path_edits, &()); self.entries_by_id.edit(entries_by_id_edits, &()); + self.scan_id = update.scan_id as usize; Ok(()) } @@ -989,6 +1253,10 @@ impl Snapshot { &self.root_name } + pub fn scan_id(&self) -> usize { + self.scan_id + } + pub fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { let path = path.as_ref(); self.traverse_from_path(true, true, path) @@ -1038,6 +1306,7 @@ impl LocalSnapshot { .map(|(path, summary)| summary.to_proto(&path.0)) .collect(), visible, + scan_id: self.scan_id as u64, } } @@ -1103,6 +1372,7 @@ impl LocalSnapshot { root_name: self.root_name().to_string(), updated_entries, removed_entries, + scan_id: self.scan_id as u64, } } @@ -1146,11 +1416,18 @@ impl LocalSnapshot { entries: impl IntoIterator, ignore: Option>, ) { - let mut parent_entry = self - .entries_by_path - .get(&PathKey(parent_path.clone()), &()) - .unwrap() - .clone(); + let mut parent_entry = if let Some(parent_entry) = + self.entries_by_path.get(&PathKey(parent_path.clone()), &()) + { + parent_entry.clone() + } else { + log::warn!( + "populating a directory {:?} that has been removed", + parent_path + ); + return; + }; + if let Some(ignore) = ignore { self.ignores.insert(parent_path, (ignore, self.scan_id)); } @@ -1210,7 +1487,7 @@ impl LocalSnapshot { if path.file_name() == Some(&GITIGNORE) { if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) { - *scan_id = self.scan_id; + *scan_id = self.snapshot.scan_id; } } } @@ -1397,7 +1674,7 @@ impl language::File for File { Worktree::Local(worktree) => { let rpc = worktree.client.clone(); let project_id = worktree.share.as_ref().map(|share| share.project_id); - let save = worktree.save(self.path.clone(), text, cx); + let save = worktree.write_file(self.path.clone(), text, cx); cx.background().spawn(async move { let entry = save.await?; if let Some(project_id) = project_id { @@ -1536,7 +1813,7 @@ pub struct Entry { pub is_ignored: bool, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EntryKind { PendingDir, Dir, @@ -1777,14 +2054,14 @@ impl BackgroundScanner { let path: Arc = Arc::from(Path::new("")); let abs_path = self.abs_path(); let (tx, rx) = channel::unbounded(); - tx.send(ScanJob { - abs_path: abs_path.to_path_buf(), - path, - ignore_stack: IgnoreStack::none(), - scan_queue: tx.clone(), - }) - .await - .unwrap(); + self.executor + .block(tx.send(ScanJob { + abs_path: abs_path.to_path_buf(), + path, + ignore_stack: IgnoreStack::none(), + scan_queue: tx.clone(), + })) + .unwrap(); drop(tx); self.executor @@ -1907,83 +2184,91 @@ impl BackgroundScanner { } async fn process_events(&mut self, mut events: Vec) -> bool { - let mut snapshot = self.snapshot(); - snapshot.scan_id += 1; + events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); + events.dedup_by(|a, b| a.path.starts_with(&b.path)); - let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&snapshot.abs_path).await { + let root_char_bag; + let root_abs_path; + let next_entry_id; + { + let mut snapshot = self.snapshot.lock(); + snapshot.scan_id += 1; + root_char_bag = snapshot.root_char_bag; + root_abs_path = snapshot.abs_path.clone(); + next_entry_id = snapshot.next_entry_id.clone(); + } + + let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&root_abs_path).await { abs_path } else { return false; }; - let root_char_bag = snapshot.root_char_bag; - let next_entry_id = snapshot.next_entry_id.clone(); - - events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); - events.dedup_by(|a, b| a.path.starts_with(&b.path)); + let metadata = futures::future::join_all( + events + .iter() + .map(|event| self.fs.metadata(&event.path)) + .collect::>(), + ) + .await; - for event in &events { - match event.path.strip_prefix(&root_abs_path) { - Ok(path) => snapshot.remove_path(&path), - Err(_) => { - log::error!( - "unexpected event {:?} for root path {:?}", - event.path, - root_abs_path - ); - continue; + // Hold the snapshot lock while clearing and re-inserting the root entries + // for each event. This way, the snapshot is not observable to the foreground + // thread while this operation is in-progress. + let (scan_queue_tx, scan_queue_rx) = channel::unbounded(); + { + let mut snapshot = self.snapshot.lock(); + for event in &events { + if let Ok(path) = event.path.strip_prefix(&root_abs_path) { + snapshot.remove_path(&path); } } - } - let (scan_queue_tx, scan_queue_rx) = channel::unbounded(); - for event in events { - let path: Arc = match event.path.strip_prefix(&root_abs_path) { - Ok(path) => Arc::from(path.to_path_buf()), - Err(_) => { - log::error!( - "unexpected event {:?} for root path {:?}", - event.path, - root_abs_path - ); - continue; - } - }; + for (event, metadata) in events.into_iter().zip(metadata.into_iter()) { + let path: Arc = match event.path.strip_prefix(&root_abs_path) { + Ok(path) => Arc::from(path.to_path_buf()), + Err(_) => { + log::error!( + "unexpected event {:?} for root path {:?}", + event.path, + root_abs_path + ); + continue; + } + }; - match self.fs.metadata(&event.path).await { - Ok(Some(metadata)) => { - let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir); - let mut fs_entry = Entry::new( - path.clone(), - &metadata, - snapshot.next_entry_id.as_ref(), - snapshot.root_char_bag, - ); - fs_entry.is_ignored = ignore_stack.is_all(); - snapshot.insert_entry(fs_entry, self.fs.as_ref()); - if metadata.is_dir { - scan_queue_tx - .send(ScanJob { - abs_path: event.path, - path, - ignore_stack, - scan_queue: scan_queue_tx.clone(), - }) - .await - .unwrap(); + match metadata { + Ok(Some(metadata)) => { + let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir); + let mut fs_entry = Entry::new( + path.clone(), + &metadata, + snapshot.next_entry_id.as_ref(), + snapshot.root_char_bag, + ); + fs_entry.is_ignored = ignore_stack.is_all(); + snapshot.insert_entry(fs_entry, self.fs.as_ref()); + if metadata.is_dir { + self.executor + .block(scan_queue_tx.send(ScanJob { + abs_path: event.path, + path, + ignore_stack, + scan_queue: scan_queue_tx.clone(), + })) + .unwrap(); + } + } + Ok(None) => {} + Err(err) => { + // TODO - create a special 'error' entry in the entries tree to mark this + log::error!("error reading file on event {:?}", err); } - } - Ok(None) => {} - Err(err) => { - // TODO - create a special 'error' entry in the entries tree to mark this - log::error!("error reading file on event {:?}", err); } } + drop(scan_queue_tx); } - *self.snapshot.lock() = snapshot; - // Scan any directories that were created as part of this event batch. - drop(scan_queue_tx); self.executor .scoped(|scope| { for _ in 0..self.executor.num_cpus() { @@ -2107,30 +2392,6 @@ impl BackgroundScanner { } } -async fn refresh_entry( - fs: &dyn Fs, - snapshot: &Mutex, - path: Arc, - abs_path: &Path, -) -> Result { - let root_char_bag; - let next_entry_id; - { - let snapshot = snapshot.lock(); - root_char_bag = snapshot.root_char_bag; - next_entry_id = snapshot.next_entry_id.clone(); - } - let entry = Entry::new( - path, - &fs.metadata(abs_path) - .await? - .ok_or_else(|| anyhow!("could not read saved file metadata"))?, - &next_entry_id, - root_char_bag, - ); - Ok(snapshot.lock().insert_entry(entry, fs)) -} - fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { let mut result = root_char_bag; result.extend( @@ -2368,7 +2629,7 @@ impl<'a> From<&'a Entry> for proto::Entry { Self { id: entry.id.to_proto(), is_dir: entry.is_dir(), - path: entry.path.to_string_lossy().to_string(), + path: entry.path.as_os_str().as_bytes().to_vec(), inode: entry.inode, mtime: Some(entry.mtime.into()), is_symlink: entry.is_symlink, @@ -2386,10 +2647,14 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { EntryKind::Dir } else { let mut char_bag = root_char_bag.clone(); - char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase())); + char_bag.extend( + String::from_utf8_lossy(&entry.path) + .chars() + .map(|c| c.to_ascii_lowercase()), + ); EntryKind::File(char_bag) }; - let path: Arc = Arc::from(Path::new(&entry.path)); + let path: Arc = PathBuf::from(OsString::from_vec(entry.path)).into(); Ok(Entry { id: ProjectEntryId::from_proto(entry.id), kind, @@ -2541,7 +2806,6 @@ mod tests { let next_entry_id = Arc::new(AtomicUsize::new(0)); let mut initial_snapshot = LocalSnapshot { abs_path: root_dir.path().into(), - scan_id: 0, removed_entry_ids: Default::default(), ignores: Default::default(), next_entry_id: next_entry_id.clone(), @@ -2551,6 +2815,7 @@ mod tests { entries_by_id: Default::default(), root_name: Default::default(), root_char_bag: Default::default(), + scan_id: 0, }, }; initial_snapshot.insert_entry( diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 4bd4f04570a1352e18b93be00437f7396370681a..e431db45ddd392f28964e073576d8df1845777c3 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -8,15 +8,19 @@ path = "src/project_panel.rs" doctest = false [dependencies] +editor = { path = "../editor" } gpui = { path = "../gpui" } project = { path = "../project" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } +postage = { version = "0.4.1", features = ["futures-traits"] } +futures = "0.3" unicase = "2.6" [dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 6f3cc17207a18980a72d34f422ae084739680202..61c97f281d327f01f1cad86bb81dac7bca92bc0f 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,15 +1,18 @@ +use editor::{Cancel, Editor}; +use futures::stream::StreamExt; use gpui::{ actions, + anyhow::{anyhow, Result}, elements::{ - Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, - Svg, UniformList, UniformListState, + ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, + ScrollTarget, Svg, UniformList, UniformListState, }, impl_internal_actions, keymap, platform::CursorStyle, - AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext, - ViewHandle, WeakViewHandle, + AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::{Entry, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; +use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; use std::{ cmp::Ordering, @@ -19,16 +22,20 @@ use std::{ }; use unicase::UniCase; use workspace::{ - menu::{SelectNext, SelectPrev}, + menu::{Confirm, SelectNext, SelectPrev}, Workspace, }; +const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; + pub struct ProjectPanel { project: ModelHandle, list: UniformListState, visible_entries: Vec<(WorktreeId, Vec)>, expanded_dir_ids: HashMap>, selection: Option, + edit_state: Option, + filename_editor: ViewHandle, handle: WeakViewHandle, } @@ -38,22 +45,46 @@ struct Selection { 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(Debug, PartialEq, Eq)] struct EntryDetails { filename: String, depth: usize, - is_dir: bool, + kind: EntryKind, is_expanded: bool, is_selected: bool, + is_editing: bool, + is_processing: bool, } #[derive(Clone)] pub struct ToggleExpanded(pub ProjectEntryId); #[derive(Clone)] -pub struct Open(pub ProjectEntryId); +pub struct Open { + pub entry_id: ProjectEntryId, + pub change_focus: bool, +} -actions!(project_panel, [ExpandSelectedEntry, CollapseSelectedEntry]); +actions!( + project_panel, + [ + ExpandSelectedEntry, + CollapseSelectedEntry, + AddDirectory, + AddFile, + Delete, + Rename + ] +); impl_internal_actions!(project_panel, [Open, ToggleExpanded]); pub fn init(cx: &mut MutableAppContext) { @@ -63,10 +94,19 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::select_prev); cx.add_action(ProjectPanel::select_next); cx.add_action(ProjectPanel::open_entry); + cx.add_action(ProjectPanel::add_file); + cx.add_action(ProjectPanel::add_directory); + cx.add_action(ProjectPanel::rename); + cx.add_async_action(ProjectPanel::delete); + cx.add_async_action(ProjectPanel::confirm); + cx.add_action(ProjectPanel::cancel); } pub enum Event { - OpenedEntry(ProjectEntryId), + OpenedEntry { + entry_id: ProjectEntryId, + focus_opened_item: bool, + }, } impl ProjectPanel { @@ -96,30 +136,55 @@ impl ProjectPanel { }) .detach(); + let filename_editor = cx.add_view(|cx| { + Editor::single_line( + Some(|theme| { + let mut style = theme.project_panel.filename_editor.clone(); + style.container.background_color.take(); + style + }), + cx, + ) + }); + let mut this = Self { project: project.clone(), list: Default::default(), visible_entries: Default::default(), expanded_dir_ids: Default::default(), selection: None, + edit_state: None, + filename_editor, handle: cx.weak_handle(), }; this.update_visible_entries(None, cx); this }); - cx.subscribe(&project_panel, move |workspace, _, event, cx| match event { - &Event::OpenedEntry(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 - .open_path( - ProjectPath { - worktree_id: worktree.read(cx).id(), - path: entry.path.clone(), - }, - cx, - ) - .detach_and_log_err(cx); + 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(), + }, + focus_opened_item, + cx, + ) + .detach_and_log_err(cx); + if !focus_opened_item { + if let Some(project_panel) = project_panel.upgrade(cx) { + cx.focus(&project_panel); + } + } + } } } } @@ -148,7 +213,10 @@ impl ProjectPanel { } } } else { - let event = Event::OpenedEntry(entry.id); + let event = Event::OpenedEntry { + entry_id: entry.id, + focus_opened_item: true, + }; cx.emit(event); } } @@ -230,8 +298,193 @@ impl ProjectPanel { } } + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { + let edit_state = self.edit_state.as_mut()?; + cx.focus_self(); + + let worktree_id = edit_state.worktree_id; + 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 edit_task; + let edited_entry_id; + + if edit_state.is_new_entry { + self.selection = Some(Selection { + worktree_id, + entry_id: NEW_ENTRY_ID, + }); + let new_path = entry.path.join(&filename); + edited_entry_id = NEW_ENTRY_ID; + edit_task = self.project.update(cx, |project, cx| { + project.create_entry((edit_state.worktree_id, new_path), edit_state.is_dir, cx) + })?; + } else { + let new_path = if let Some(parent) = entry.path.clone().parent() { + parent.join(&filename) + } else { + filename.clone().into() + }; + edited_entry_id = entry.id; + edit_task = self.project.update(cx, |project, cx| { + project.rename_entry(entry.id, new_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.update_visible_entries(None, 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(); + cx.notify(); + } + fn open_entry(&mut self, action: &Open, cx: &mut ViewContext) { - cx.emit(Event::OpenedEntry(action.0)); + cx.emit(Event::OpenedEntry { + entry_id: action.entry_id, + focus_opened_item: action.change_focus, + }); + } + + fn add_file(&mut self, _: &AddFile, cx: &mut ViewContext) { + self.add_entry(false, cx) + } + + fn add_directory(&mut self, _: &AddDirectory, 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)); + cx.focus(&self.filename_editor); + self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), 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 filename = entry + .path + .file_name() + .map_or(String::new(), |s| s.to_string_lossy().to_string()); + self.filename_editor.update(cx, |editor, cx| { + editor.set_text(filename, cx); + editor.select_all(&Default::default(), cx); + }); + cx.focus(&self.filename_editor); + self.update_visible_entries(None, cx); + cx.notify(); + } + } + } + } + + 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.next().await != Some(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) { @@ -346,11 +599,35 @@ impl ProjectPanel { } }; + 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(false); - while let Some(item) = entry_iter.entry() { - visible_worktree_entries.push(item.clone()); - if expanded_dir_ids.binary_search(&item.id).is_err() { + 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, + }); + } + if expanded_dir_ids.binary_search(&entry.id).is_err() { if entry_iter.advance_to_sibling() { continue; } @@ -436,6 +713,7 @@ impl ProjectPanel { if ix >= range.end { return; } + if ix + visible_worktree_entries.len() <= range.start { ix += visible_worktree_entries.len(); continue; @@ -452,16 +730,42 @@ impl ProjectPanel { let root_name = OsStr::new(snapshot.root_name()); for entry in &visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix] { - let filename = entry.path.file_name().unwrap_or(root_name); - let details = EntryDetails { - filename: filename.to_string_lossy().to_string(), + let mut details = EntryDetails { + filename: entry + .path + .file_name() + .unwrap_or(root_name) + .to_string_lossy() + .to_string(), depth: entry.path.components().count(), - is_dir: entry.is_dir(), + kind: entry.kind, is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), is_selected: self.selection.map_or(false, |e| { e.worktree_id == snapshot.id() && e.entry_id == entry.id }), + is_editing: false, + is_processing: false, }; + 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); } } @@ -472,63 +776,73 @@ impl ProjectPanel { fn render_entry( entry_id: ProjectEntryId, details: EntryDetails, + editor: &ViewHandle, theme: &theme::ProjectPanel, cx: &mut ViewContext, ) -> ElementBox { - let is_dir = details.is_dir; + let kind = details.kind; + let show_editor = details.is_editing && !details.is_processing; MouseEventHandler::new::(entry_id.to_usize(), cx, |state, _| { - let style = match (details.is_selected, state.hovered) { - (false, false) => &theme.entry, - (false, true) => &theme.hovered_entry, - (true, false) => &theme.selected_entry, - (true, true) => &theme.hovered_selected_entry, + let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width; + let style = theme.entry.style_for(state, details.is_selected); + let row_container_style = if show_editor { + theme.filename_editor.container + } else { + style.container }; Flex::row() .with_child( - ConstrainedBox::new( - Align::new( - ConstrainedBox::new(if is_dir { - if details.is_expanded { - Svg::new("icons/disclosure-open.svg") - .with_color(style.icon_color) - .boxed() - } else { - Svg::new("icons/disclosure-closed.svg") - .with_color(style.icon_color) - .boxed() - } - } else { - Empty::new().boxed() - }) - .with_max_width(style.icon_size) - .with_max_height(style.icon_size) - .boxed(), - ) - .boxed(), - ) + ConstrainedBox::new(if kind == EntryKind::Dir { + if details.is_expanded { + Svg::new("icons/disclosure-open.svg") + .with_color(style.icon_color) + .boxed() + } else { + Svg::new("icons/disclosure-closed.svg") + .with_color(style.icon_color) + .boxed() + } + } else { + Empty::new().boxed() + }) + .with_max_width(style.icon_size) + .with_max_height(style.icon_size) + .aligned() + .constrained() .with_width(style.icon_size) .boxed(), ) - .with_child( + .with_child(if show_editor { + ChildView::new(editor.clone()) + .contained() + .with_margin_left(theme.entry.default.icon_spacing) + .aligned() + .left() + .flex(1.0, true) + .boxed() + } else { Label::new(details.filename, style.text.clone()) .contained() .with_margin_left(style.icon_spacing) .aligned() .left() - .boxed(), - ) + .boxed() + }) .constrained() - .with_height(theme.entry.height) + .with_height(theme.entry.default.height) .contained() - .with_style(style.container) - .with_padding_left(theme.container.padding.left + details.depth as f32 * 20.) + .with_style(row_container_style) + .with_padding_left(padding) .boxed() }) - .on_click(move |cx| { - if is_dir { + .on_click(move |click_count, cx| { + if kind == EntryKind::Dir { cx.dispatch_action(ToggleExpanded(entry_id)) } else { - cx.dispatch_action(Open(entry_id)) + cx.dispatch_action(Open { + entry_id, + change_focus: click_count > 1, + }) } }) .with_cursor_style(CursorStyle::PointingHand) @@ -556,8 +870,14 @@ impl View for ProjectPanel { let theme = cx.global::().theme.clone(); let this = handle.upgrade(cx).unwrap(); this.update(cx.app, |this, cx| { - this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| { - items.push(Self::render_entry(entry, details, &theme.project_panel, cx)); + this.for_each_visible_entry(range.clone(), cx, |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + &theme.project_panel, + cx, + )); }); }) }, @@ -584,6 +904,7 @@ impl Entity for ProjectPanel { mod tests { use super::*; use gpui::{TestAppContext, ViewHandle}; + use project::FakeFs; use serde_json::json; use std::{collections::HashSet, path::Path}; use workspace::WorkspaceParams; @@ -592,8 +913,7 @@ mod tests { async fn test_visible_list(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let params = cx.update(WorkspaceParams::test); - let fs = params.fs.as_fake(); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/root1", json!({ @@ -630,241 +950,408 @@ mod tests { ) .await; - let project = cx.update(|cx| { - Project::local( - params.client.clone(), - params.user_store.clone(), - params.languages.clone(), - params.fs.clone(), - cx, - ) - }); - let (root1, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root1", true, cx) - }) - .await - .unwrap(); - root1 - .read_with(cx, |t, _| t.as_local().unwrap().scan_complete()) - .await; - let (root2, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/root2", true, cx) - }) - .await - .unwrap(); - root2 - .read_with(cx, |t, _| t.as_local().unwrap().scan_complete()) - .await; - + let project = Project::test(fs.clone(), ["/root1", "/root2"], cx).await; + let params = cx.update(WorkspaceParams::test); let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx)); assert_eq!( - visible_entry_details(&panel, 0..50, cx), + visible_entries_as_strings(&panel, 0..50, cx), &[ - EntryDetails { - filename: "root1".to_string(), - depth: 0, - is_dir: true, - is_expanded: true, - is_selected: false, - }, - EntryDetails { - filename: "a".to_string(), - depth: 1, - is_dir: true, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "b".to_string(), - depth: 1, - is_dir: true, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "C".to_string(), - depth: 1, - is_dir: true, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: ".dockerignore".to_string(), - depth: 1, - is_dir: false, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "root2".to_string(), - depth: 0, - is_dir: true, - is_expanded: true, - is_selected: false - }, - EntryDetails { - filename: "d".to_string(), - depth: 1, - is_dir: true, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: "e".to_string(), - depth: 1, - is_dir: true, - is_expanded: false, - is_selected: false - } - ], + "v root1", + " > a", + " > b", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] ); toggle_expand_dir(&panel, "root1/b", cx); assert_eq!( - visible_entry_details(&panel, 0..50, cx), + visible_entries_as_strings(&panel, 0..50, cx), &[ - EntryDetails { - filename: "root1".to_string(), - depth: 0, - is_dir: true, - is_expanded: true, - is_selected: false, - }, - EntryDetails { - filename: "a".to_string(), - depth: 1, - is_dir: true, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "b".to_string(), - depth: 1, - is_dir: true, - is_expanded: true, - is_selected: true, - }, - EntryDetails { - filename: "3".to_string(), - depth: 2, - is_dir: true, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "4".to_string(), - depth: 2, - is_dir: true, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "C".to_string(), - depth: 1, - is_dir: true, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: ".dockerignore".to_string(), - depth: 1, - is_dir: false, - is_expanded: false, - is_selected: false, + "v root1", + " > a", + " v b <== selected", + " > 3", + " > 4", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + assert_eq!( + visible_entries_as_strings(&panel, 5..8, cx), + &[ + // + " > C", + " .dockerignore", + "v root2", + ] + ); + } + + #[gpui::test(iterations = 30)] + async fn test_editing_files(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root1", + json!({ + ".dockerignore": "", + ".git": { + "HEAD": "", }, - EntryDetails { - filename: "root2".to_string(), - depth: 0, - is_dir: true, - is_expanded: true, - is_selected: false + "a": { + "0": { "q": "", "r": "", "s": "" }, + "1": { "t": "", "u": "" }, + "2": { "v": "", "w": "", "x": "", "y": "" }, }, - EntryDetails { - filename: "d".to_string(), - depth: 1, - is_dir: true, - is_expanded: false, - is_selected: false + "b": { + "3": { "Q": "" }, + "4": { "R": "", "S": "", "T": "", "U": "" }, }, - EntryDetails { - filename: "e".to_string(), - depth: 1, - is_dir: true, - is_expanded: false, - is_selected: false + "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", "/root2"], cx).await; + let params = cx.update(WorkspaceParams::test); + let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); + let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx)); + + select_path(&panel, "root1", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1 <== selected", + " > 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.add_file(&AddFile, cx)); + assert!(panel.read_with(cx, |panel, cx| panel.filename_editor.is_focused(cx))); assert_eq!( - visible_entry_details(&panel, 5..8, cx), - [ - EntryDetails { - filename: "C".to_string(), - depth: 1, - is_dir: true, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: ".dockerignore".to_string(), - depth: 1, - is_dir: false, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: "root2".to_string(), - depth: 0, - is_dir: true, - is_expanded: true, - is_selected: false - } + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > a", + " > b", + " > C", + " [EDITOR: ''] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", ] ); - fn toggle_expand_dir( - panel: &ViewHandle, - path: impl AsRef, - cx: &mut TestAppContext, - ) { - let path = path.as_ref(); - panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees(cx).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(&ToggleExpanded(entry_id), cx); - return; - } + 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", + " > 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", + " > a", + " > b", + " > C", + " .dockerignore", + " the-new-filename <== selected", + "v root2", + " > d", + " > e", + ] + ); + + select_path(&panel, "root1/b", cx); + panel.update(cx, |panel, cx| panel.add_file(&AddFile, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..9, cx), + &[ + "v root1", + " > 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", cx)); + panel.confirm(&Confirm, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..9, cx), + &[ + "v root1", + " > a", + " v b", + " > 3", + " > 4", + " another-filename <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + select_path(&panel, "root1/b/another-filename", cx); + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..9, cx), + &[ + "v root1", + " > a", + " v b", + " > 3", + " > 4", + " [EDITOR: 'another-filename'] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + let confirm = panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("a-different-filename", cx)); + panel.confirm(&Confirm, cx).unwrap() + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..9, cx), + &[ + "v root1", + " > a", + " v b", + " > 3", + " > 4", + " [PROCESSING: 'a-different-filename'] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..9, cx), + &[ + "v root1", + " > a", + " v b", + " > 3", + " > 4", + " a-different-filename <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + panel.update(cx, |panel, cx| panel.add_directory(&AddDirectory, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..9, cx), + &[ + "v root1", + " > a", + " v b", + " > [EDITOR: ''] <== selected", + " > 3", + " > 4", + " a-different-filename", + " > 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..9, cx), + &[ + "v root1", + " > a", + " v b", + " > [PROCESSING: 'new-dir']", + " > 3 <== selected", + " > 4", + " a-different-filename", + " > C", + " .dockerignore", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..9, cx), + &[ + "v root1", + " > a", + " v b", + " > 3 <== selected", + " > 4", + " > new-dir", + " a-different-filename", + " > C", + " .dockerignore", + ] + ); + } + + fn toggle_expand_dir( + panel: &ViewHandle, + path: impl AsRef, + cx: &mut TestAppContext, + ) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for worktree in panel.project.read(cx).worktrees(cx).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(&ToggleExpanded(entry_id), cx); + return; } - panic!("no worktree for path {:?}", path); - }); - } + } + panic!("no worktree for path {:?}", path); + }); + } + + fn select_path( + panel: &ViewHandle, + path: impl AsRef, + cx: &mut TestAppContext, + ) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for worktree in panel.project.read(cx).worktrees(cx).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_entry_details( - panel: &ViewHandle, - range: Range, - cx: &mut TestAppContext, - ) -> Vec { - let mut result = Vec::new(); - let mut project_entries = HashSet::new(); - panel.update(cx, |panel, cx| { - panel.for_each_visible_entry(range, cx, |project_entry, details, _| { + fn visible_entries_as_strings( + panel: &ViewHandle, + 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 ); - result.push(details); - }); + } + + let indent = " ".repeat(details.depth); + let icon = if matches!(details.kind, EntryKind::Dir | EntryKind::PendingDir) { + 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 - } + result } } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index a35d44a81049c175158e7d6287c501dddcf0f517..4f083888d9d04d55e8c35f8ecea3afe1446c9491 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -297,23 +297,12 @@ mod tests { let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({ "test.rs": "" })).await; - let project = Project::test(fs.clone(), cx); - project.update(cx, |project, _| { - project.languages().add(Arc::new(language)); - }); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); + let project = Project::test(fs.clone(), ["/dir"], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); let _buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "test.rs"), cx) + project.open_local_buffer("/dir/test.rs", cx) }) .await .unwrap(); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index bf18db9e2ba7d5efce655617e68aa018364af99a..fa0b587df486957d65b87c97caaf5b9a3eb5ed8f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -36,57 +36,63 @@ message Envelope { RegisterWorktree register_worktree = 28; UnregisterWorktree unregister_worktree = 29; UpdateWorktree update_worktree = 31; - UpdateDiagnosticSummary update_diagnostic_summary = 32; - StartLanguageServer start_language_server = 33; - UpdateLanguageServer update_language_server = 34; - - OpenBufferById open_buffer_by_id = 35; - OpenBufferByPath open_buffer_by_path = 36; - OpenBufferResponse open_buffer_response = 37; - UpdateBuffer update_buffer = 38; - UpdateBufferFile update_buffer_file = 39; - SaveBuffer save_buffer = 40; - BufferSaved buffer_saved = 41; - BufferReloaded buffer_reloaded = 42; - ReloadBuffers reload_buffers = 43; - ReloadBuffersResponse reload_buffers_response = 44; - FormatBuffers format_buffers = 45; - FormatBuffersResponse format_buffers_response = 46; - GetCompletions get_completions = 47; - GetCompletionsResponse get_completions_response = 48; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 49; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 50; - GetCodeActions get_code_actions = 51; - GetCodeActionsResponse get_code_actions_response = 52; - ApplyCodeAction apply_code_action = 53; - ApplyCodeActionResponse apply_code_action_response = 54; - PrepareRename prepare_rename = 55; - PrepareRenameResponse prepare_rename_response = 56; - PerformRename perform_rename = 57; - PerformRenameResponse perform_rename_response = 58; - SearchProject search_project = 59; - SearchProjectResponse search_project_response = 60; - - GetChannels get_channels = 61; - GetChannelsResponse get_channels_response = 62; - JoinChannel join_channel = 63; - JoinChannelResponse join_channel_response = 64; - LeaveChannel leave_channel = 65; - SendChannelMessage send_channel_message = 66; - SendChannelMessageResponse send_channel_message_response = 67; - ChannelMessageSent channel_message_sent = 68; - GetChannelMessages get_channel_messages = 69; - GetChannelMessagesResponse get_channel_messages_response = 70; - - UpdateContacts update_contacts = 71; - - GetUsers get_users = 72; - GetUsersResponse get_users_response = 73; - - Follow follow = 74; - FollowResponse follow_response = 75; - UpdateFollowers update_followers = 76; - Unfollow unfollow = 77; + + CreateProjectEntry create_project_entry = 32; + RenameProjectEntry rename_project_entry = 33; + DeleteProjectEntry delete_project_entry = 34; + ProjectEntryResponse project_entry_response = 35; + + UpdateDiagnosticSummary update_diagnostic_summary = 36; + StartLanguageServer start_language_server = 37; + UpdateLanguageServer update_language_server = 38; + + OpenBufferById open_buffer_by_id = 39; + OpenBufferByPath open_buffer_by_path = 40; + OpenBufferResponse open_buffer_response = 41; + UpdateBuffer update_buffer = 42; + UpdateBufferFile update_buffer_file = 43; + SaveBuffer save_buffer = 44; + BufferSaved buffer_saved = 45; + BufferReloaded buffer_reloaded = 46; + ReloadBuffers reload_buffers = 47; + ReloadBuffersResponse reload_buffers_response = 48; + FormatBuffers format_buffers = 49; + FormatBuffersResponse format_buffers_response = 50; + GetCompletions get_completions = 51; + GetCompletionsResponse get_completions_response = 52; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 53; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 54; + GetCodeActions get_code_actions = 55; + GetCodeActionsResponse get_code_actions_response = 56; + ApplyCodeAction apply_code_action = 57; + ApplyCodeActionResponse apply_code_action_response = 58; + PrepareRename prepare_rename = 59; + PrepareRenameResponse prepare_rename_response = 60; + PerformRename perform_rename = 61; + PerformRenameResponse perform_rename_response = 62; + SearchProject search_project = 63; + SearchProjectResponse search_project_response = 64; + + GetChannels get_channels = 65; + GetChannelsResponse get_channels_response = 66; + JoinChannel join_channel = 67; + JoinChannelResponse join_channel_response = 68; + LeaveChannel leave_channel = 69; + SendChannelMessage send_channel_message = 70; + SendChannelMessageResponse send_channel_message_response = 71; + ChannelMessageSent channel_message_sent = 72; + GetChannelMessages get_channel_messages = 73; + GetChannelMessagesResponse get_channel_messages_response = 74; + + UpdateContacts update_contacts = 75; + + GetUsers get_users = 76; + GetUsersResponse get_users_response = 77; + + Follow follow = 78; + FollowResponse follow_response = 79; + UpdateFollowers update_followers = 80; + Unfollow unfollow = 81; } } @@ -156,6 +162,30 @@ message UpdateWorktree { string root_name = 3; repeated Entry updated_entries = 4; repeated uint64 removed_entries = 5; + uint64 scan_id = 6; +} + +message CreateProjectEntry { + uint64 project_id = 1; + uint64 worktree_id = 2; + bytes path = 3; + bool is_directory = 4; +} + +message RenameProjectEntry { + uint64 project_id = 1; + uint64 entry_id = 2; + bytes new_path = 3; +} + +message DeleteProjectEntry { + uint64 project_id = 1; + uint64 entry_id = 2; +} + +message ProjectEntryResponse { + Entry entry = 1; + uint64 worktree_scan_id = 2; } message AddProjectCollaborator { @@ -630,6 +660,7 @@ message Worktree { repeated Entry entries = 3; repeated DiagnosticSummary diagnostic_summaries = 4; bool visible = 5; + uint64 scan_id = 6; } message File { @@ -642,7 +673,7 @@ message File { message Entry { uint64 id = 1; bool is_dir = 2; - string path = 3; + bytes path = 3; uint64 inode = 4; Timestamp mtime = 5; bool is_symlink = 6; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 98fc493774041ddc84fe0a054ff713651c3db887..c505869c554744e1c89911b0a9c5f556ac3dd8b0 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -6,13 +6,14 @@ use prost::Message as _; use serde::Serialize; use std::any::{Any, TypeId}; use std::{ + fmt::Debug, io, time::{Duration, SystemTime, UNIX_EPOCH}, }; include!(concat!(env!("OUT_DIR"), "/zed.messages.rs")); -pub trait EnvelopedMessage: Clone + Serialize + Sized + Send + Sync + 'static { +pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 'static { const NAME: &'static str; const PRIORITY: MessagePriority; fn into_envelope( @@ -147,6 +148,8 @@ messages!( (BufferReloaded, Foreground), (BufferSaved, Foreground), (ChannelMessageSent, Foreground), + (CreateProjectEntry, Foreground), + (DeleteProjectEntry, Foreground), (Error, Foreground), (Follow, Foreground), (FollowResponse, Foreground), @@ -174,8 +177,6 @@ messages!( (JoinChannelResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), - (StartLanguageServer, Foreground), - (UpdateLanguageServer, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), (OpenBufferById, Background), @@ -187,6 +188,7 @@ messages!( (PerformRenameResponse, Background), (PrepareRename, Background), (PrepareRenameResponse, Background), + (ProjectEntryResponse, Foreground), (RegisterProjectResponse, Foreground), (Ping, Foreground), (RegisterProject, Foreground), @@ -194,12 +196,14 @@ messages!( (ReloadBuffers, Foreground), (ReloadBuffersResponse, Foreground), (RemoveProjectCollaborator, Foreground), + (RenameProjectEntry, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), (SendChannelMessage, Foreground), (SendChannelMessageResponse, Foreground), (ShareProject, Foreground), + (StartLanguageServer, Foreground), (Test, Foreground), (Unfollow, Foreground), (UnregisterProject, Foreground), @@ -210,6 +214,7 @@ messages!( (UpdateContacts, Foreground), (UpdateDiagnosticSummary, Foreground), (UpdateFollowers, Foreground), + (UpdateLanguageServer, Foreground), (UpdateWorktree, Foreground), ); @@ -219,6 +224,8 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), + (CreateProjectEntry, ProjectEntryResponse), + (DeleteProjectEntry, ProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), @@ -241,6 +248,7 @@ request_messages!( (RegisterProject, RegisterProjectResponse), (RegisterWorktree, Ack), (ReloadBuffers, ReloadBuffersResponse), + (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), (SendChannelMessage, SendChannelMessageResponse), @@ -257,6 +265,9 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, + CreateProjectEntry, + RenameProjectEntry, + DeleteProjectEntry, Follow, FormatBuffers, GetCodeActions, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index e0f6a0133c88a086248288784f4ff530ae7e9066..ffddcb9cd3ce1ac232b8eacd6b4ebeb08580c1b4 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -5,4 +5,4 @@ pub mod proto; pub use conn::Connection; pub use peer::*; -pub const PROTOCOL_VERSION: u32 = 15; +pub const PROTOCOL_VERSION: u32 = 16; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d1f17608f28c98f4e805e865890211104bd8449a..549edf89e71cdc356f20b3911b39922323cbef19 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -292,7 +292,7 @@ impl BufferSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option))) + .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(search_option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -316,7 +316,7 @@ impl BufferSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |cx| match direction { + .on_click(move |_, cx| match direction { Direction::Prev => cx.dispatch_action(SelectPrevMatch), Direction::Next => cx.dispatch_action(SelectNextMatch), }) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 39870a31d5c0f3722045e736638ea7c2f964e5fc..cbd373c468f700533d24065568f41e54960a4141 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -666,7 +666,7 @@ impl ProjectSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |cx| match direction { + .on_click(move |_, cx| match direction { Direction::Prev => cx.dispatch_action(SelectPrevMatch), Direction::Next => cx.dispatch_action(SelectNextMatch), }) @@ -693,7 +693,7 @@ impl ProjectSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) + .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -844,16 +844,7 @@ mod tests { }), ) .await; - let project = Project::test(fs.clone(), cx); - let (tree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - + let project = Project::test(fs.clone(), ["/dir"], cx).await; let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); let search_view = cx.add_view(Default::default(), |cx| { ProjectSearchView::new(search.clone(), cx) diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index c77b10e1bd110f15bc9e1def5f63b204c968a85a..193786112b46f5dec84c0343ca8e3c9d5c24926a 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -483,17 +483,20 @@ impl PartialEq for SumTree { impl Eq for SumTree {} impl SumTree { - pub fn insert_or_replace(&mut self, item: T, cx: &::Context) -> bool { - let mut replaced = false; + pub fn insert_or_replace( + &mut self, + item: T, + cx: &::Context, + ) -> Option { + let mut replaced = None; *self = { let mut cursor = self.cursor::(); let mut new_tree = cursor.slice(&item.key(), Bias::Left, cx); - if cursor - .item() - .map_or(false, |cursor_item| cursor_item.key() == item.key()) - { - cursor.next(cx); - replaced = true; + if let Some(cursor_item) = cursor.item() { + if cursor_item.key() == item.key() { + replaced = Some(cursor_item.clone()); + cursor.next(cx); + } } new_tree.push(item, cx); new_tree.push_tree(cursor.suffix(cx), cx); @@ -502,6 +505,23 @@ impl SumTree { replaced } + pub fn remove(&mut self, key: &T::Key, cx: &::Context) -> Option { + let mut removed = None; + *self = { + let mut cursor = self.cursor::(); + let mut new_tree = cursor.slice(key, Bias::Left, cx); + if let Some(item) = cursor.item() { + if item.key() == *key { + removed = Some(item.clone()); + cursor.next(cx); + } + } + new_tree.push_tree(cursor.suffix(cx), cx); + new_tree + }; + removed + } + pub fn edit( &mut self, mut edits: Vec>, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c0959a0e5e89b03fb972ecb0b1c431012f66878d..d64c093144d752c1cb6a88105c3bd7cfea18be7c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -204,14 +204,13 @@ pub struct ChatPanel { pub hovered_sign_in_prompt: TextStyle, } -#[derive(Debug, Deserialize, Default)] +#[derive(Deserialize, Default)] pub struct ProjectPanel { #[serde(flatten)] pub container: ContainerStyle, - pub entry: ProjectPanelEntry, - pub hovered_entry: ProjectPanelEntry, - pub selected_entry: ProjectPanelEntry, - pub hovered_selected_entry: ProjectPanelEntry, + pub entry: Interactive, + pub filename_editor: FieldEditor, + pub indent_width: f32, } #[derive(Debug, Deserialize, Default)] diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 400a8e467a881c78c427c82f4e1e7414cf0d2dba..f99fceef3a504228c63fba785b6125013e280b79 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -50,7 +50,7 @@ impl<'a> VimTestContext<'a> { let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let item = workspace - .update(cx, |workspace, cx| workspace.open_path(file, cx)) + .update(cx, |workspace, cx| workspace.open_path(file, true, cx)) .await .expect("Could not open test file"); diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index ddc6d893086dfe71d73fa57f887800fa64445749..f58e0b973e05e27db44359b95c11524010e14ea2 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -168,7 +168,7 @@ impl View for LspStatus { self.failed.join(", "), if self.failed.len() > 1 { "s" } else { "" } ); - handler = Some(|cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage)); + handler = Some(|_, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage)); } else { return Empty::new().boxed(); } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a992897c1182c9e03a5ee515d6a71fd7cf2bc8d7..e963ca7e02ab4d1f2d8179fcb37658f371926f8f 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -59,7 +59,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { - pane.activate_item(action.0, true, cx); + pane.activate_item(action.0, true, true, cx); }); cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { pane.activate_prev_item(cx); @@ -213,7 +213,7 @@ impl Pane { { let prev_active_item_index = pane.active_item_index; pane.nav_history.borrow_mut().set_mode(mode); - pane.activate_item(index, true, cx); + pane.activate_item(index, true, true, cx); pane.nav_history .borrow_mut() .set_mode(NavigationMode::Normal); @@ -257,6 +257,7 @@ impl Pane { workspace, pane.clone(), project_entry_id, + true, cx, build_item, ) @@ -287,6 +288,7 @@ impl Pane { workspace: &mut Workspace, pane: ViewHandle, project_entry_id: ProjectEntryId, + focus_item: bool, cx: &mut ViewContext, build_item: impl FnOnce(&mut MutableAppContext) -> Box, ) -> Box { @@ -294,7 +296,7 @@ impl Pane { for (ix, item) in pane.items.iter().enumerate() { if item.project_entry_id(cx) == Some(project_entry_id) { let item = item.boxed_clone(); - pane.activate_item(ix, true, cx); + pane.activate_item(ix, true, focus_item, cx); return Some(item); } } @@ -304,7 +306,7 @@ impl Pane { existing_item } else { let item = build_item(cx); - Self::add_item(workspace, pane, item.boxed_clone(), true, cx); + Self::add_item(workspace, pane, item.boxed_clone(), true, focus_item, cx); item } } @@ -313,12 +315,15 @@ impl Pane { workspace: &mut Workspace, pane: ViewHandle, item: Box, - local: bool, + activate_pane: bool, + focus_item: bool, cx: &mut ViewContext, ) { // Prevent adding the same item to the pane more than once. if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) { - pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx)); + pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, activate_pane, focus_item, cx) + }); return; } @@ -327,7 +332,7 @@ impl Pane { pane.update(cx, |pane, cx| { let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len()); pane.items.insert(item_idx, item); - pane.activate_item(item_idx, local, cx); + pane.activate_item(item_idx, activate_pane, focus_item, cx); cx.notify(); }); } @@ -378,7 +383,13 @@ impl Pane { self.items.iter().position(|i| i.id() == item.id()) } - pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext) { + pub fn activate_item( + &mut self, + index: usize, + activate_pane: bool, + focus_item: bool, + cx: &mut ViewContext, + ) { use NavigationMode::{GoingBack, GoingForward}; if index < self.items.len() { let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); @@ -387,11 +398,15 @@ impl Pane { && prev_active_item_ix < self.items.len()) { self.items[prev_active_item_ix].deactivated(cx); - cx.emit(Event::ActivateItem { local }); + cx.emit(Event::ActivateItem { + local: activate_pane, + }); } self.update_toolbar(cx); - if local { + if focus_item { self.focus_active_item(cx); + } + if activate_pane { self.activate(cx); } self.autoscroll = true; @@ -406,7 +421,7 @@ impl Pane { } else if self.items.len() > 0 { index = self.items.len() - 1; } - self.activate_item(index, true, cx); + self.activate_item(index, true, true, cx); } pub fn activate_next_item(&mut self, cx: &mut ViewContext) { @@ -416,7 +431,7 @@ impl Pane { } else { index = 0; } - self.activate_item(index, true, cx); + self.activate_item(index, true, true, cx); } fn close_active_item( @@ -498,7 +513,7 @@ impl Pane { if is_last_item_for_entry { if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) { let mut answer = pane.update(&mut cx, |pane, cx| { - pane.activate_item(item_to_close_ix, true, cx); + pane.activate_item(item_to_close_ix, true, true, cx); cx.prompt( PromptLevel::Warning, CONFLICT_MESSAGE, @@ -518,7 +533,7 @@ impl Pane { } else if cx.read(|cx| item.is_dirty(cx)) { if cx.read(|cx| item.can_save(cx)) { let mut answer = pane.update(&mut cx, |pane, cx| { - pane.activate_item(item_to_close_ix, true, cx); + pane.activate_item(item_to_close_ix, true, true, cx); cx.prompt( PromptLevel::Warning, DIRTY_MESSAGE, @@ -535,7 +550,7 @@ impl Pane { } } else if cx.read(|cx| item.can_save_as(cx)) { let mut answer = pane.update(&mut cx, |pane, cx| { - pane.activate_item(item_to_close_ix, true, cx); + pane.activate_item(item_to_close_ix, true, true, cx); cx.prompt( PromptLevel::Warning, DIRTY_MESSAGE, @@ -737,7 +752,7 @@ impl Pane { .with_cursor_style(CursorStyle::PointingHand) .on_click({ let pane = pane.clone(); - move |cx| { + move |_, cx| { cx.dispatch_action(CloseItem { item_id, pane: pane.clone(), @@ -949,7 +964,7 @@ mod tests { let close_items = workspace.update(cx, |workspace, cx| { pane.update(cx, |pane, cx| { - pane.activate_item(1, true, cx); + pane.activate_item(1, true, true, cx); assert_eq!(pane.active_item().unwrap().id(), item2.id()); }); diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 2f13469cec47ca08b7904c1d3885523932cd277a..bc7314e73286b48ed7f2ca95bb56cd7e93f71de8 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -203,7 +203,7 @@ impl View for SidebarButtons { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |cx| { + .on_click(move |_, cx| { cx.dispatch_action(ToggleSidebarItem { side, item_index: ix, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d072c515503edb02ad6f52d9c55c87ca850d02d8..b23710834dfd38f00edcaa3ae7cf454613b79d6b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -493,7 +493,7 @@ impl ItemHandle for ViewHandle { if T::should_activate_item_on_event(event) { pane.update(cx, |pane, cx| { if let Some(ix) = pane.index_for_item(&item) { - pane.activate_item(ix, true, cx); + pane.activate_item(ix, true, true, cx); pane.activate(cx); } }); @@ -898,7 +898,7 @@ impl Workspace { if fs.is_file(&abs_path).await { Some( this.update(&mut cx, |this, cx| { - this.open_path(project_path, cx) + this.open_path(project_path, true, cx) }) .await, ) @@ -1065,7 +1065,7 @@ impl Workspace { Side::Right => &mut self.right_sidebar, }; let active_item = sidebar.update(cx, |sidebar, cx| { - sidebar.toggle_item(action.item_index, cx); + sidebar.activate_item(action.item_index, cx); sidebar.active_item().cloned() }); if let Some(active_item) = active_item { @@ -1099,12 +1099,13 @@ impl Workspace { pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { let pane = self.active_pane().clone(); - Pane::add_item(self, pane, item, true, cx); + Pane::add_item(self, pane, item, true, true, cx); } pub fn open_path( &mut self, path: impl Into, + focus_item: bool, cx: &mut ViewContext, ) -> Task, Arc>> { let pane = self.active_pane().downgrade(); @@ -1119,6 +1120,7 @@ impl Workspace { this, pane, project_entry_id, + focus_item, cx, build_item, )) @@ -1187,7 +1189,7 @@ impl Workspace { }); if let Some((pane, ix)) = result { self.activate_pane(pane.clone(), cx); - pane.update(cx, |pane, cx| pane.activate_item(ix, true, cx)); + pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx)); true } else { false @@ -1277,7 +1279,7 @@ impl Workspace { self.activate_pane(new_pane.clone(), cx); if let Some(item) = pane.read(cx).active_item() { if let Some(clone) = item.clone_on_split(cx.as_mut()) { - Pane::add_item(self, new_pane.clone(), clone, true, cx); + Pane::add_item(self, new_pane.clone(), clone, true, true, cx); } } self.center.split(&pane, &new_pane, direction).unwrap(); @@ -1584,7 +1586,7 @@ impl Workspace { .with_style(style.container) .boxed() }) - .on_click(|cx| cx.dispatch_action(Authenticate)) + .on_click(|_, cx| cx.dispatch_action(Authenticate)) .with_cursor_style(CursorStyle::PointingHand) .aligned() .boxed(), @@ -1635,7 +1637,7 @@ impl Workspace { if let Some(peer_id) = peer_id { MouseEventHandler::new::(replica_id.into(), cx, move |_, _| content) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |cx| cx.dispatch_action(ToggleFollow(peer_id))) + .on_click(move |_, cx| cx.dispatch_action(ToggleFollow(peer_id))) .boxed() } else { content @@ -1667,7 +1669,7 @@ impl Workspace { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|cx| cx.dispatch_action(ToggleShare)) + .on_click(|_, cx| cx.dispatch_action(ToggleShare)) .boxed(), ) } else { @@ -1961,7 +1963,7 @@ impl Workspace { } for (pane, item) in items_to_add { - Pane::add_item(self, pane.clone(), item.boxed_clone(), false, cx); + Pane::add_item(self, pane.clone(), item.boxed_clone(), false, false, cx); if pane == self.active_pane { pane.update(cx, |pane, cx| pane.focus_active_item(cx)); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 20b56268345dd77bf95ed6589fefc4d03b17a0fc..77e400e02f96d276002018c63048a1fd133d5b2d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -446,7 +446,7 @@ mod tests { // Open the first entry let entry_1 = workspace - .update(cx, |w, cx| w.open_path(file1.clone(), cx)) + .update(cx, |w, cx| w.open_path(file1.clone(), true, cx)) .await .unwrap(); cx.read(|cx| { @@ -460,7 +460,7 @@ mod tests { // Open the second entry workspace - .update(cx, |w, cx| w.open_path(file2.clone(), cx)) + .update(cx, |w, cx| w.open_path(file2.clone(), true, cx)) .await .unwrap(); cx.read(|cx| { @@ -474,7 +474,7 @@ mod tests { // Open the first entry again. The existing pane item is activated. let entry_1b = workspace - .update(cx, |w, cx| w.open_path(file1.clone(), cx)) + .update(cx, |w, cx| w.open_path(file1.clone(), true, cx)) .await .unwrap(); assert_eq!(entry_1.id(), entry_1b.id()); @@ -492,7 +492,7 @@ mod tests { workspace .update(cx, |w, cx| { w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx); - w.open_path(file2.clone(), cx) + w.open_path(file2.clone(), true, cx) }) .await .unwrap(); @@ -511,8 +511,8 @@ mod tests { // Open the third entry twice concurrently. Only one pane item is added. let (t1, t2) = workspace.update(cx, |w, cx| { ( - w.open_path(file3.clone(), cx), - w.open_path(file3.clone(), cx), + w.open_path(file3.clone(), true, cx), + w.open_path(file3.clone(), true, cx), ) }); t1.await.unwrap(); @@ -780,6 +780,7 @@ mod tests { worktree_id: worktree.read(cx).id(), path: Path::new("the-new-name.rs").into(), }, + true, cx, ) }) @@ -875,7 +876,7 @@ mod tests { let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); workspace - .update(cx, |w, cx| w.open_path(file1.clone(), cx)) + .update(cx, |w, cx| w.open_path(file1.clone(), true, cx)) .await .unwrap(); @@ -955,7 +956,7 @@ mod tests { let file3 = entries[2].clone(); let editor1 = workspace - .update(cx, |w, cx| w.open_path(file1.clone(), cx)) + .update(cx, |w, cx| w.open_path(file1.clone(), true, cx)) .await .unwrap() .downcast::() @@ -964,13 +965,13 @@ mod tests { editor.select_display_ranges(&[DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)], cx); }); let editor2 = workspace - .update(cx, |w, cx| w.open_path(file2.clone(), cx)) + .update(cx, |w, cx| w.open_path(file2.clone(), true, cx)) .await .unwrap() .downcast::() .unwrap(); let editor3 = workspace - .update(cx, |w, cx| w.open_path(file3.clone(), cx)) + .update(cx, |w, cx| w.open_path(file3.clone(), true, cx)) .await .unwrap() .downcast::() diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 068fd547366bfac4953f8d8cbac2e43d280473d2..bacc3590e552c5900e113825229574af6c82e98d 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -1,34 +1,35 @@ import Theme from "../themes/theme"; -import { Color } from "../utils/color"; import { panel } from "./app"; -import { backgroundColor, iconColor, text, TextColor } from "./components"; +import { backgroundColor, iconColor, player, text } from "./components"; export default function projectPanel(theme: Theme) { - function entry(theme: Theme, textColor: TextColor, background?: Color) { - return { + return { + ...panel, + padding: { left: 12, right: 12, top: 6, bottom: 6 }, + indentWidth: 20, + entry: { height: 24, - background, iconColor: iconColor(theme, "muted"), iconSize: 8, iconSpacing: 8, - text: text(theme, "mono", textColor, { size: "sm" }), - }; - } - - return { - ...panel, - entry: entry(theme, "muted"), - hoveredEntry: entry( - theme, - "primary", - backgroundColor(theme, 300, "hovered") - ), - selectedEntry: entry(theme, "primary"), - hoveredSelectedEntry: entry( - theme, - "active", - backgroundColor(theme, 300, "hovered") - ), - padding: { left: 12, right: 12, top: 6, bottom: 6 }, + text: text(theme, "mono", "muted", { size: "sm" }), + hover: { + background: backgroundColor(theme, 300, "hovered"), + text: text(theme, "mono", "primary", { size: "sm" }), + }, + active: { + background: backgroundColor(theme, 300, "active"), + text: text(theme, "mono", "primary", { size: "sm" }), + }, + activeHover: { + background: backgroundColor(theme, 300, "hovered"), + text: text(theme, "mono", "active", { size: "sm" }), + } + }, + filenameEditor: { + background: backgroundColor(theme, 500, "active"), + text: text(theme, "mono", "primary", { size: "sm" }), + selection: player(theme, 1).selection, + }, }; }