From 12d45f165520e1651fde4ec9a08dc6b2157a85ac Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Apr 2022 11:08:25 -0700 Subject: [PATCH 01/26] Clean up project panel theme --- assets/themes/cave-dark.json | 60 ++++++++++------------- assets/themes/cave-light.json | 60 ++++++++++------------- assets/themes/dark.json | 60 ++++++++++------------- assets/themes/light.json | 60 ++++++++++------------- assets/themes/solarized-dark.json | 60 ++++++++++------------- assets/themes/solarized-light.json | 60 ++++++++++------------- assets/themes/sulphurpool-dark.json | 60 ++++++++++------------- assets/themes/sulphurpool-light.json | 60 ++++++++++------------- crates/project_panel/src/project_panel.rs | 56 ++++++++++----------- crates/theme/src/theme.rs | 6 +-- styles/src/styleTree/projectPanel.ts | 44 ++++++++--------- 11 files changed, 247 insertions(+), 339 deletions(-) diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 4b53480c79..0dde598727 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,30 @@ "family": "Zed Mono", "color": "#8b8792", "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, - "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 + }, + "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 + } } } }, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 54e4e6eac8..495371914a 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,30 @@ "family": "Zed Mono", "color": "#585260", "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, - "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 + }, + "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 + } } } }, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 91b0e57ac2..528e3ca91f 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,30 @@ "family": "Zed Mono", "color": "#808080", "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, - "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 + }, + "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 + } } } }, diff --git a/assets/themes/light.json b/assets/themes/light.json index b66a8fe9ec..240b2627c8 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,30 @@ "family": "Zed Mono", "color": "#636363", "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, - "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 + }, + "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 + } } } }, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 92d340a9b4..52c3e753c2 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,30 @@ "family": "Zed Mono", "color": "#93a1a1", "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, - "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 + }, + "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 + } } } }, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index ee5acf2712..f11c1515a3 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,30 @@ "family": "Zed Mono", "color": "#586e75", "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, - "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 + }, + "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 + } } } }, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 7ae3c88703..023dc64ddd 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,30 @@ "family": "Zed Mono", "color": "#979db4", "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, - "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 + }, + "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 + } } } }, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 69eb194e3c..38679f2a96 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,30 @@ "family": "Zed Mono", "color": "#5e6687", "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, - "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 + }, + "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 + } } } }, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 6f3cc17207..a98445d0b0 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,8 +1,9 @@ +use editor::Editor; use gpui::{ actions, elements::{ - Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, - Svg, UniformList, UniformListState, + ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, Svg, + UniformList, UniformListState, }, impl_internal_actions, keymap, platform::CursorStyle, @@ -477,35 +478,26 @@ impl ProjectPanel { ) -> ElementBox { let is_dir = details.is_dir; 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 style = theme.entry.style_for(state, details.is_selected); 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 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) + .aligned() + .constrained() .with_width(style.icon_size) .boxed(), ) @@ -518,10 +510,12 @@ impl ProjectPanel { .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_padding_left( + theme.container.padding.left + details.depth as f32 * theme.indent_width, + ) .boxed() }) .on_click(move |cx| { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c0959a0e5e..c2b1fc26a0 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -208,10 +208,8 @@ pub struct ChatPanel { 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 indent_width: f32, } #[derive(Debug, Deserialize, Default)] diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 068fd54736..55a1e1b81c 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -1,34 +1,30 @@ import Theme from "../themes/theme"; -import { Color } from "../utils/color"; import { panel } from "./app"; -import { backgroundColor, iconColor, text, TextColor } from "./components"; +import { backgroundColor, iconColor, 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" }), + } + }, }; } From d4492086b3d50cef7807c011dcbe519642cdb163 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Apr 2022 12:12:51 -0700 Subject: [PATCH 02/26] Abstract more local project setup inside Project::test helper --- crates/editor/src/editor.rs | 26 +- crates/project/src/project.rs | 506 +++++------------- crates/project_panel/src/project_panel.rs | 35 +- crates/project_symbols/src/project_symbols.rs | 17 +- crates/search/src/project_search.rs | 11 +- 5 files changed, 156 insertions(+), 439 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f47636c093..440a5d5346 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/project/src/project.rs b/crates/project/src/project.rs index a9f24f2b61..f8e25ff2cc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -452,12 +452,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> { @@ -850,6 +865,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 +906,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 +938,7 @@ impl Project { }) } - fn open_local_buffer( + fn open_local_buffer_internal( &mut self, path: &Arc, worktree: &ModelHandle, @@ -928,7 +955,7 @@ impl Project { }) } - fn open_remote_buffer( + fn open_remote_buffer_internal( &mut self, path: &Arc, worktree: &ModelHandle, @@ -4905,6 +4932,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 +4947,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 +4973,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 +5057,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 +5074,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 +5121,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 +5151,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(); @@ -5382,34 +5392,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 +5503,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 +5556,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 +5625,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 +5694,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 +5963,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 +6054,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 +6208,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 +6331,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 +6364,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 +6458,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 +6516,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 +6622,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 +6651,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 +6673,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 +6697,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 +6720,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 +6746,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 +6758,7 @@ mod tests { cx, ) }); + // tree load_task.await; cx.read(|cx| { @@ -7005,7 +6852,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 +6860,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 +6884,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 +6894,21 @@ mod tests { #[gpui::test] async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { - use std::fs; + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "file1": "abc", + "file2": "def", + "file3": "ghi", + }), + ) + .await; - 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()); - - 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 +6978,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 +6989,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 +7001,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 +7012,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 +7028,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 +7054,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 +7070,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 +7088,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 +7122,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 +7386,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 +7505,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 +7518,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_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index a98445d0b0..ff14cffe7c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,4 +1,3 @@ -use editor::Editor; use gpui::{ actions, elements::{ @@ -578,6 +577,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; @@ -586,8 +586,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!({ @@ -624,34 +623,8 @@ 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!( diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index a35d44a810..4f083888d9 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/search/src/project_search.rs b/crates/search/src/project_search.rs index 39870a31d5..d3b3520a53 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -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) From a217e2e64b15c5640910e7ca527ba524f3758985 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Apr 2022 16:27:45 -0700 Subject: [PATCH 03/26] Implement basic AddFile command in project panel --- Cargo.lock | 1 + crates/project/src/worktree.rs | 2 +- crates/project_panel/Cargo.toml | 2 + crates/project_panel/src/project_panel.rs | 537 +++++++++++++++++++--- 4 files changed, 465 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23a87887a5..d74f09da1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3329,6 +3329,7 @@ dependencies = [ name = "project_panel" version = "0.1.0" dependencies = [ + "editor", "gpui", "project", "serde_json", diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 19b490b0e2..b7a075395b 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -672,7 +672,7 @@ impl LocalWorktree { }) } - fn save( + pub fn save( &self, path: impl Into>, text: Rope, diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 4bd4f04570..caedaef8b1 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -8,6 +8,7 @@ path = "src/project_panel.rs" doctest = false [dependencies] +editor = { path = "../editor" } gpui = { path = "../gpui" } project = { path = "../project" } settings = { path = "../settings" } @@ -17,6 +18,7 @@ workspace = { path = "../workspace" } unicase = "2.6" [dev-dependencies] +editor = { path = "../editor", feature = ["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 ff14cffe7c..a28fc5e46e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,13 +1,15 @@ +use editor::{Cancel, Editor}; use gpui::{ actions, + anyhow::Result, elements::{ - 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, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use project::{Entry, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; @@ -19,7 +21,7 @@ use std::{ }; use unicase::UniCase; use workspace::{ - menu::{SelectNext, SelectPrev}, + menu::{Confirm, SelectNext, SelectPrev}, Workspace, }; @@ -29,6 +31,8 @@ pub struct ProjectPanel { visible_entries: Vec<(WorktreeId, Vec)>, expanded_dir_ids: HashMap>, selection: Option, + edit_state: Option, + filename_editor: ViewHandle, handle: WeakViewHandle, } @@ -38,22 +42,40 @@ struct Selection { entry_id: ProjectEntryId, } +#[derive(Copy, Clone, Debug)] +struct EditState { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + new_file: bool, +} + #[derive(Debug, PartialEq, Eq)] struct EntryDetails { filename: String, depth: usize, - is_dir: bool, + kind: EntryKind, is_expanded: bool, is_selected: bool, } +#[derive(Debug, PartialEq, Eq)] +enum EntryKind { + File, + Dir, + FileRenameEditor, + NewFileEditor, +} + #[derive(Clone)] pub struct ToggleExpanded(pub ProjectEntryId); #[derive(Clone)] pub struct Open(pub ProjectEntryId); -actions!(project_panel, [ExpandSelectedEntry, CollapseSelectedEntry]); +actions!( + project_panel, + [ExpandSelectedEntry, CollapseSelectedEntry, AddFile] +); impl_internal_actions!(project_panel, [Open, ToggleExpanded]); pub fn init(cx: &mut MutableAppContext) { @@ -63,6 +85,9 @@ 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_async_action(ProjectPanel::confirm); + cx.add_action(ProjectPanel::cancel); } pub enum Event { @@ -96,12 +121,22 @@ impl ProjectPanel { }) .detach(); + let editor = cx.add_view(|cx| Editor::single_line(None, cx)); + cx.subscribe(&editor, |this, _, event, cx| { + if let editor::Event::Blurred = event { + this.editor_blurred(cx); + } + }) + .detach(); + 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: editor, handle: cx.weak_handle(), }; this.update_visible_entries(None, cx); @@ -230,10 +265,114 @@ impl ProjectPanel { } } + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { + let edit_state = self.edit_state.take()?; + cx.focus_self(); + let worktree = self + .project + .read(cx) + .worktree_for_id(edit_state.worktree_id, cx)?; + + // TODO - implement this for remote projects + if !worktree.read(cx).is_local() { + return None; + } + + let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?; + let filename = self.filename_editor.read(cx).text(cx); + + if edit_state.new_file { + let new_path = entry.path.join(filename); + let save = worktree.update(cx, |worktree, cx| { + worktree + .as_local() + .unwrap() + .save(new_path, Default::default(), cx) + }); + Some(cx.spawn(|this, mut cx| async move { + save.await?; + this.update(&mut cx, |this, cx| { + this.update_visible_entries(None, cx); + cx.notify(); + }); + Ok(()) + })) + } else { + // TODO - implement + None + } + } + + 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)); } + fn add_file(&mut self, _: &AddFile, 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, + new_file: true, + }); + self.filename_editor + .update(cx, |editor, cx| editor.clear(cx)); + cx.focus(&self.filename_editor); + self.update_visible_entries(None, cx); + } + cx.notify(); + } + + fn editor_blurred(&mut self, cx: &mut ViewContext) { + self.edit_state = None; + self.update_visible_entries(None, cx); + cx.focus_self(); + cx.notify(); + } + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { if let Some(selection) = self.selection { let (mut worktree_ix, mut entry_ix, _) = @@ -346,11 +485,30 @@ impl ProjectPanel { } }; + let new_file_parent_id = self.edit_state.and_then(|edit_state| { + if edit_state.worktree_id == worktree_id && edit_state.new_file { + Some(edit_state.entry_id) + } else { + None + } + }); + 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_file_parent_id { + visible_worktree_entries.push(Entry { + id: entry.id, + kind: project::EntryKind::File(Default::default()), + 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 +594,7 @@ impl ProjectPanel { if ix >= range.end { return; } + if ix + visible_worktree_entries.len() <= range.start { ix += visible_worktree_entries.len(); continue; @@ -452,16 +611,39 @@ 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: if entry.is_dir() { + EntryKind::Dir + } else { + EntryKind::File + }, 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 }), }; + if let Some(edit_state) = self.edit_state { + if edit_state.worktree_id == *worktree_id && edit_state.entry_id == entry.id + { + if edit_state.new_file { + if entry.is_file() { + details.kind = EntryKind::NewFileEditor; + details.filename = Default::default(); + details.is_expanded = false; + details.is_selected = false; + } + } else { + details.kind = EntryKind::FileRenameEditor; + } + } + } callback(entry.id, details, cx); } } @@ -472,15 +654,29 @@ 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 padding = theme.container.padding.left + details.depth as f32 * theme.indent_width; + + if kind == EntryKind::FileRenameEditor || kind == EntryKind::NewFileEditor { + return ChildView::new(editor.clone()) + .constrained() + .with_height(theme.entry.default.height) + .contained() + .with_margin_left( + padding + theme.entry.default.icon_spacing + theme.entry.default.icon_size, + ) + .boxed(); + } + MouseEventHandler::new::(entry_id.to_usize(), cx, |state, _| { let style = theme.entry.style_for(state, details.is_selected); Flex::row() .with_child( - ConstrainedBox::new(if is_dir { + ConstrainedBox::new(if kind == EntryKind::Dir { if details.is_expanded { Svg::new("icons/disclosure-open.svg") .with_color(style.icon_color) @@ -512,13 +708,11 @@ impl ProjectPanel { .with_height(theme.entry.default.height) .contained() .with_style(style.container) - .with_padding_left( - theme.container.padding.left + details.depth as f32 * theme.indent_width, - ) + .with_padding_left(padding) .boxed() }) .on_click(move |cx| { - if is_dir { + if kind == EntryKind::Dir { cx.dispatch_action(ToggleExpanded(entry_id)) } else { cx.dispatch_action(Open(entry_id)) @@ -549,8 +743,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, + )); }); }) }, @@ -633,59 +833,59 @@ mod tests { EntryDetails { filename: "root1".to_string(), depth: 0, - is_dir: true, + kind: EntryKind::Dir, is_expanded: true, is_selected: false, }, EntryDetails { filename: "a".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false, }, EntryDetails { filename: "b".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false, }, EntryDetails { filename: "C".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false, }, EntryDetails { filename: ".dockerignore".to_string(), depth: 1, - is_dir: false, + kind: EntryKind::File, is_expanded: false, is_selected: false, }, EntryDetails { filename: "root2".to_string(), depth: 0, - is_dir: true, + kind: EntryKind::Dir, is_expanded: true, is_selected: false }, EntryDetails { filename: "d".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false }, EntryDetails { filename: "e".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false - } + }, ], ); @@ -696,73 +896,73 @@ mod tests { EntryDetails { filename: "root1".to_string(), depth: 0, - is_dir: true, + kind: EntryKind::Dir, is_expanded: true, is_selected: false, }, EntryDetails { filename: "a".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false, }, EntryDetails { filename: "b".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: true, is_selected: true, }, EntryDetails { filename: "3".to_string(), depth: 2, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false, }, EntryDetails { filename: "4".to_string(), depth: 2, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false, }, EntryDetails { filename: "C".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false, }, EntryDetails { filename: ".dockerignore".to_string(), depth: 1, - is_dir: false, + kind: EntryKind::File, is_expanded: false, is_selected: false, }, EntryDetails { filename: "root2".to_string(), depth: 0, - is_dir: true, + kind: EntryKind::Dir, is_expanded: true, is_selected: false }, EntryDetails { filename: "d".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false }, EntryDetails { filename: "e".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false - } + }, ] ); @@ -772,66 +972,251 @@ mod tests { EntryDetails { filename: "C".to_string(), depth: 1, - is_dir: true, + kind: EntryKind::Dir, is_expanded: false, is_selected: false }, EntryDetails { filename: ".dockerignore".to_string(), depth: 1, - is_dir: false, + kind: EntryKind::File, is_expanded: false, is_selected: false }, EntryDetails { filename: "root2".to_string(), depth: 0, - is_dir: true, + kind: EntryKind::Dir, is_expanded: true, is_selected: false + }, + ] + ); + } + + #[gpui::test] + 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": "", + }, + "a": { + "0": { "q": "", "r": "", "s": "" }, + "1": { "t": "", "u": "" }, + "2": { "v": "", "w": "", "x": "", "y": "" }, + }, + "b": { + "3": { "Q": "" }, + "4": { "R": "", "S": "", "T": "", "U": "" }, + }, + "C": { + "5": {}, + "6": { "V": "", "W": "" }, + "7": { "X": "" }, + "8": { "Y": {}, "Z": "" } } + }), + ) + .await; + fs.insert_tree( + "/root2", + json!({ + "d": { + "9": "" + }, + "e": {} + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1", "/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", ] ); - 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); - }); - } + // 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_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1 <== selected", + " > a", + " > b", + " > C", + " [NEW FILE EDITOR]", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); - 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, _| { + panel.update(cx, |panel, cx| { + panel.filename_editor.update(cx, |editor, cx| { + editor.set_text("the-new-filename", cx); + }); + panel.confirm(&Confirm, cx); + }); + cx.foreground().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1 <== selected", + " > a", + " > b", + " > C", + " .dockerignore", + " the-new-filename", + "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 <== selected", + " > 3", + " > 4", + " [NEW FILE EDITOR]", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + } + + 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); + }); + } + + 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(); + let mut has_editor = false; + panel.update(cx, |panel, cx| { + panel.for_each_visible_entry(range, cx, |project_entry, details, _| { + if details.kind == EntryKind::NewFileEditor + || details.kind == EntryKind::FileRenameEditor + { + 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); - }); + } + result.push(details) }); + }); - result - } + result + } + + fn visible_entries_as_strings( + panel: &ViewHandle, + range: Range, + cx: &mut TestAppContext, + ) -> Vec { + visible_entry_details(panel, range, cx) + .into_iter() + .map(|details| { + let indent = " ".repeat(details.depth); + let icon = if details.kind == EntryKind::Dir { + if details.is_expanded { + "v " + } else { + "> " + } + } else { + " " + }; + let name = if details.kind == EntryKind::FileRenameEditor { + "[RENAME EDITOR]" + } else if details.kind == EntryKind::NewFileEditor { + "[NEW FILE EDITOR]" + } else { + &details.filename + }; + let selected = if details.is_selected { + " <== selected" + } else { + "" + }; + format!("{indent}{icon}{name}{selected}") + }) + .collect() } } From 333b4aaf4e49ebd5a47f2247ba6e4bae1bfd00a6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Apr 2022 17:17:04 -0700 Subject: [PATCH 04/26] Implement Rename command in project panel --- crates/project/src/worktree.rs | 32 ++++++ crates/project_panel/Cargo.toml | 2 +- crates/project_panel/src/project_panel.rs | 132 ++++++++++++++++++++-- 3 files changed, 154 insertions(+), 12 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b7a075395b..812783aaaf 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -694,6 +694,38 @@ impl LocalWorktree { }) } + pub fn rename( + &self, + old_path: impl Into>, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Task> { + let old_path = old_path.into(); + let new_path = new_path.into(); + let abs_old_path = self.absolutize(&old_path); + let abs_new_path = self.absolutize(&new_path); + let background_snapshot = self.background_snapshot.clone(); + let fs = self.fs.clone(); + let rename = cx.background().spawn(async move { + fs.rename(&abs_old_path, &abs_new_path, Default::default()) + .await?; + background_snapshot.lock().remove_path(&old_path); + refresh_entry( + fs.as_ref(), + &background_snapshot, + new_path.clone(), + &abs_new_path, + ) + .await + }); + + cx.spawn(|this, mut cx| async move { + let entry = rename.await?; + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + Ok(entry) + }) + } + pub fn register( &mut self, project_id: u64, diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index caedaef8b1..4b78f2a1fa 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -18,7 +18,7 @@ workspace = { path = "../workspace" } unicase = "2.6" [dev-dependencies] -editor = { path = "../editor", feature = ["test-support"] } +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 a28fc5e46e..37f14d7962 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -74,7 +74,7 @@ pub struct Open(pub ProjectEntryId); actions!( project_panel, - [ExpandSelectedEntry, CollapseSelectedEntry, AddFile] + [ExpandSelectedEntry, CollapseSelectedEntry, AddFile, Rename] ); impl_internal_actions!(project_panel, [Open, ToggleExpanded]); @@ -86,6 +86,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::select_next); cx.add_action(ProjectPanel::open_entry); cx.add_action(ProjectPanel::add_file); + cx.add_action(ProjectPanel::rename); cx.add_async_action(ProjectPanel::confirm); cx.add_action(ProjectPanel::cancel); } @@ -298,8 +299,23 @@ impl ProjectPanel { Ok(()) })) } else { - // TODO - implement - None + let old_path = entry.path.clone(); + let new_path = if let Some(parent) = old_path.parent() { + parent.join(filename) + } else { + filename.into() + }; + let rename = worktree.update(cx, |worktree, cx| { + worktree.as_local().unwrap().rename(old_path, new_path, cx) + }); + Some(cx.spawn(|this, mut cx| async move { + let new_entry = rename.await?; + this.update(&mut cx, |this, cx| { + this.update_visible_entries(Some((edit_state.worktree_id, new_entry.id)), cx); + cx.notify(); + }); + Ok(()) + })) } } @@ -362,8 +378,35 @@ impl ProjectPanel { .update(cx, |editor, cx| editor.clear(cx)); cx.focus(&self.filename_editor); self.update_visible_entries(None, 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, + new_file: false, + }); + 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)); + cx.focus(&self.filename_editor); + self.update_visible_entries(None, cx); + cx.notify(); + } + } } - cx.notify(); } fn editor_blurred(&mut self, cx: &mut ViewContext) { @@ -1074,13 +1117,15 @@ mod tests { ] ); - panel.update(cx, |panel, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("the-new-filename", cx); - }); - panel.confirm(&Confirm, cx); - }); - cx.foreground().run_until_parked(); + panel + .update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); + panel.confirm(&Confirm, cx).unwrap() + }) + .await + .unwrap(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ @@ -1112,6 +1157,71 @@ mod tests { " 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 <== selected", + " > 3", + " > 4", + " another-filename", + " > 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", + " [RENAME EDITOR] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + panel + .update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("a-different-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", + " a-different-filename <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); } fn toggle_expand_dir( From 8fdc5c9be36dc3c85365acefe72e6d5bc6d6db7b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 May 2022 13:19:58 -0700 Subject: [PATCH 05/26] Improve the appearance of project panel filename editor * Always layout single-line editors with a fixed height * Preserve directory chevron when editing folder names * Allow theming the filename editor Co-authored-by: Antonio Scandurra --- assets/keymaps/default.json | 3 +- assets/themes/cave-dark.json | 12 + assets/themes/cave-light.json | 12 + assets/themes/dark.json | 12 + assets/themes/light.json | 12 + assets/themes/solarized-dark.json | 12 + assets/themes/solarized-light.json | 12 + assets/themes/sulphurpool-dark.json | 12 + assets/themes/sulphurpool-light.json | 12 + crates/editor/src/element.rs | 6 + crates/project/src/project.rs | 2 + crates/project/src/worktree.rs | 2 +- crates/project_panel/src/project_panel.rs | 321 ++++++---------------- crates/theme/src/theme.rs | 3 +- styles/src/styleTree/projectPanel.ts | 7 +- 15 files changed, 206 insertions(+), 234 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1378022bcf..b7d390062f 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -331,7 +331,8 @@ "context": "ProjectPanel", "bindings": { "left": "project_panel::CollapseSelectedEntry", - "right": "project_panel::ExpandSelectedEntry" + "right": "project_panel::ExpandSelectedEntry", + "f2": "project_panel::Rename" } } ] \ No newline at end of file diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 0dde598727..3b19c2e63e 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#26232a5c", + "text": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + } } }, "chat_panel": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 495371914a..2e33fb774f 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#e2dfe72e", + "text": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + } } }, "chat_panel": { diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 528e3ca91f..ba9b7189d3 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#ffffff1f", + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + } } }, "chat_panel": { diff --git a/assets/themes/light.json b/assets/themes/light.json index 240b2627c8..7cbd315c8a 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#0000000f", + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + } } }, "chat_panel": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 52c3e753c2..8672518b4c 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#0736425c", + "text": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + } } }, "chat_panel": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index f11c1515a3..66b43e613d 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#eee8d52e", + "text": { + "family": "Zed Mono", + "color": "#073642", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + } } }, "chat_panel": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 023dc64ddd..66f5182172 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#2932565c", + "text": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + } } }, "chat_panel": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 38679f2a96..34a3389728 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#dfe2f12e", + "text": { + "family": "Zed Mono", + "color": "#293256", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + } } }, "chat_panel": { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 28ef9e5af0..f4453774f7 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); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f8e25ff2cc..891f963e1d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -225,6 +225,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)) } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 812783aaaf..7651e332da 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1568,7 +1568,7 @@ pub struct Entry { pub is_ignored: bool, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EntryKind { PendingDir, Dir, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 37f14d7962..d038ac941f 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -11,7 +11,7 @@ use gpui::{ AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, 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, @@ -25,6 +25,8 @@ use workspace::{ Workspace, }; +const NEW_FILE_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; + pub struct ProjectPanel { project: ModelHandle, list: UniformListState, @@ -56,14 +58,7 @@ struct EntryDetails { kind: EntryKind, is_expanded: bool, is_selected: bool, -} - -#[derive(Debug, PartialEq, Eq)] -enum EntryKind { - File, - Dir, - FileRenameEditor, - NewFileEditor, + is_editing: bool, } #[derive(Clone)] @@ -122,8 +117,17 @@ impl ProjectPanel { }) .detach(); - let editor = cx.add_view(|cx| Editor::single_line(None, cx)); - cx.subscribe(&editor, |this, _, event, cx| { + 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, + ) + }); + cx.subscribe(&filename_editor, |this, _, event, cx| { if let editor::Event::Blurred = event { this.editor_blurred(cx); } @@ -137,7 +141,7 @@ impl ProjectPanel { expanded_dir_ids: Default::default(), selection: None, edit_state: None, - filename_editor: editor, + filename_editor, handle: cx.weak_handle(), }; this.update_visible_entries(None, cx); @@ -399,8 +403,10 @@ impl ProjectPanel { .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)); + 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(); @@ -542,7 +548,7 @@ impl ProjectPanel { visible_worktree_entries.push(entry.clone()); if Some(entry.id) == new_file_parent_id { visible_worktree_entries.push(Entry { - id: entry.id, + id: NEW_FILE_ENTRY_ID, kind: project::EntryKind::File(Default::default()), path: entry.path.join("\0").into(), inode: 0, @@ -662,30 +668,24 @@ impl ProjectPanel { .to_string_lossy() .to_string(), depth: entry.path.components().count(), - kind: if entry.is_dir() { - EntryKind::Dir - } else { - EntryKind::File - }, + 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, }; if let Some(edit_state) = self.edit_state { - if edit_state.worktree_id == *worktree_id && edit_state.entry_id == entry.id - { - if edit_state.new_file { - if entry.is_file() { - details.kind = EntryKind::NewFileEditor; - details.filename = Default::default(); - details.is_expanded = false; - details.is_selected = false; - } - } else { - details.kind = EntryKind::FileRenameEditor; + if edit_state.new_file { + if entry.id == NEW_FILE_ENTRY_ID { + details.is_editing = true; + details.filename.clear(); } - } + } else { + if entry.id == edit_state.entry_id { + details.is_editing = true; + } + }; } callback(entry.id, details, cx); } @@ -702,21 +702,14 @@ impl ProjectPanel { cx: &mut ViewContext, ) -> ElementBox { let kind = details.kind; - let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width; - - if kind == EntryKind::FileRenameEditor || kind == EntryKind::NewFileEditor { - return ChildView::new(editor.clone()) - .constrained() - .with_height(theme.entry.default.height) - .contained() - .with_margin_left( - padding + theme.entry.default.icon_spacing + theme.entry.default.icon_size, - ) - .boxed(); - } - MouseEventHandler::new::(entry_id.to_usize(), cx, |state, _| { + 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 details.is_editing { + theme.filename_editor.container + } else { + style.container + }; Flex::row() .with_child( ConstrainedBox::new(if kind == EntryKind::Dir { @@ -739,18 +732,26 @@ impl ProjectPanel { .with_width(style.icon_size) .boxed(), ) - .with_child( + .with_child(if details.is_editing { + 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.default.height) .contained() - .with_style(style.container) + .with_style(row_container_style) .with_padding_left(padding) .boxed() }) @@ -871,168 +872,43 @@ mod tests { 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, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: false, - }, - EntryDetails { - filename: "a".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "b".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "C".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: ".dockerignore".to_string(), - depth: 1, - kind: EntryKind::File, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "root2".to_string(), - depth: 0, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: false - }, - EntryDetails { - filename: "d".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: "e".to_string(), - depth: 1, - kind: EntryKind::Dir, - 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, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: false, - }, - EntryDetails { - filename: "a".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "b".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: true, - }, - EntryDetails { - filename: "3".to_string(), - depth: 2, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "4".to_string(), - depth: 2, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "C".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: ".dockerignore".to_string(), - depth: 1, - kind: EntryKind::File, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "root2".to_string(), - depth: 0, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: false - }, - EntryDetails { - filename: "d".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: "e".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false - }, + "v root1", + " > a", + " v b <== selected", + " > 3", + " > 4", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", ] ); assert_eq!( - visible_entry_details(&panel, 5..8, cx), - [ - EntryDetails { - filename: "C".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: ".dockerignore".to_string(), - depth: 1, - kind: EntryKind::File, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: "root2".to_string(), - depth: 0, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: false - }, + visible_entries_as_strings(&panel, 5..8, cx), + &[ + // + " > C", + " .dockerignore", + "v root2", ] ); } @@ -1109,7 +985,7 @@ mod tests { " > a", " > b", " > C", - " [NEW FILE EDITOR]", + " [EDITOR: '']", " .dockerignore", "v root2", " > d", @@ -1151,7 +1027,7 @@ mod tests { " v b <== selected", " > 3", " > 4", - " [NEW FILE EDITOR]", + " [EDITOR: '']", " > C", " .dockerignore", " the-new-filename", @@ -1192,7 +1068,7 @@ mod tests { " v b", " > 3", " > 4", - " [RENAME EDITOR] <== selected", + " [EDITOR: 'another-filename'] <== selected", " > C", " .dockerignore", " the-new-filename", @@ -1265,19 +1141,17 @@ mod tests { }); } - fn visible_entry_details( + fn visible_entries_as_strings( panel: &ViewHandle, range: Range, cx: &mut TestAppContext, - ) -> Vec { + ) -> 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.kind == EntryKind::NewFileEditor - || details.kind == EntryKind::FileRenameEditor - { + if details.is_editing { assert!(!has_editor, "duplicate editor entry"); has_editor = true; } else { @@ -1288,21 +1162,7 @@ mod tests { details ); } - result.push(details) - }); - }); - result - } - - fn visible_entries_as_strings( - panel: &ViewHandle, - range: Range, - cx: &mut TestAppContext, - ) -> Vec { - visible_entry_details(panel, range, cx) - .into_iter() - .map(|details| { let indent = " ".repeat(details.depth); let icon = if details.kind == EntryKind::Dir { if details.is_expanded { @@ -1313,10 +1173,9 @@ mod tests { } else { " " }; - let name = if details.kind == EntryKind::FileRenameEditor { - "[RENAME EDITOR]" - } else if details.kind == EntryKind::NewFileEditor { - "[NEW FILE EDITOR]" + let editor_text = format!("[EDITOR: '{}']", details.filename); + let name = if details.is_editing { + &editor_text } else { &details.filename }; @@ -1325,8 +1184,10 @@ mod tests { } else { "" }; - format!("{indent}{icon}{name}{selected}") - }) - .collect() + result.push(format!("{indent}{icon}{name}{selected}")); + }); + }); + + result } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c2b1fc26a0..d64c093144 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -204,11 +204,12 @@ 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: Interactive, + pub filename_editor: FieldEditor, pub indent_width: f32, } diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 55a1e1b81c..bacc3590e5 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -1,6 +1,6 @@ import Theme from "../themes/theme"; import { panel } from "./app"; -import { backgroundColor, iconColor, text } from "./components"; +import { backgroundColor, iconColor, player, text } from "./components"; export default function projectPanel(theme: Theme) { return { @@ -26,5 +26,10 @@ export default function projectPanel(theme: Theme) { 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, + }, }; } From a19766931d65669776d216eef1c2d9a063889d7e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 May 2022 17:16:55 -0700 Subject: [PATCH 06/26] Rename entry atomically in LocalWorktree::rename --- crates/project/src/worktree.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 7651e332da..8116a8b973 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -628,7 +628,8 @@ impl LocalWorktree { 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 = + refresh_entry(fs.as_ref(), &background_snapshot, path, &abs_path, None).await?; this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); Ok(( File { @@ -684,7 +685,14 @@ impl LocalWorktree { 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 + refresh_entry( + fs.as_ref(), + &background_snapshot, + path.clone(), + &abs_path, + None, + ) + .await }); cx.spawn(|this, mut cx| async move { @@ -709,12 +717,12 @@ impl LocalWorktree { let rename = cx.background().spawn(async move { fs.rename(&abs_old_path, &abs_new_path, Default::default()) .await?; - background_snapshot.lock().remove_path(&old_path); refresh_entry( fs.as_ref(), &background_snapshot, new_path.clone(), &abs_new_path, + Some(old_path), ) .await }); @@ -2144,6 +2152,7 @@ async fn refresh_entry( snapshot: &Mutex, path: Arc, abs_path: &Path, + old_path: Option>, ) -> Result { let root_char_bag; let next_entry_id; @@ -2160,7 +2169,11 @@ async fn refresh_entry( &next_entry_id, root_char_bag, ); - Ok(snapshot.lock().insert_entry(entry, fs)) + let mut snapshot = snapshot.lock(); + if let Some(old_path) = old_path { + snapshot.remove_path(&old_path); + } + Ok(snapshot.insert_entry(entry, fs)) } fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { From 0ff39f1280e32eb14b3ca3f153db7865f555547c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 May 2022 17:56:09 -0700 Subject: [PATCH 07/26] Select new files in the project panel after creating them --- crates/project/src/fs.rs | 11 +++++++++-- crates/project_panel/src/project_panel.rs | 12 ++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index ec7925685d..bc815f5be1 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; } @@ -647,9 +647,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_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d038ac941f..001667d311 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -295,9 +295,9 @@ impl ProjectPanel { .save(new_path, Default::default(), cx) }); Some(cx.spawn(|this, mut cx| async move { - save.await?; + let new_entry = save.await?; this.update(&mut cx, |this, cx| { - this.update_visible_entries(None, cx); + this.update_visible_entries(Some((edit_state.worktree_id, new_entry.id)), cx); cx.notify(); }); Ok(()) @@ -1005,12 +1005,12 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ - "v root1 <== selected", + "v root1", " > a", " > b", " > C", " .dockerignore", - " the-new-filename", + " the-new-filename <== selected", "v root2", " > d", " > e", @@ -1048,10 +1048,10 @@ mod tests { &[ "v root1", " > a", - " v b <== selected", + " v b", " > 3", " > 4", - " another-filename", + " another-filename <== selected", " > C", " .dockerignore", " the-new-filename", From 8291b8108df8d082d1e4da015d89d57441114332 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 May 2022 15:36:45 -0700 Subject: [PATCH 08/26] Update snapshot atomically when processing FS events --- crates/project/src/project.rs | 1 + crates/project/src/worktree.rs | 146 ++++++++++++---------- crates/project_panel/src/project_panel.rs | 2 +- 3 files changed, 79 insertions(+), 70 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 891f963e1d..97697c26f2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5282,6 +5282,7 @@ mod tests { language_id: Default::default() }, ); + // We clear the diagnostics, since the language has changed. rust_buffer2.read_with(cx, |buffer, _| { assert_eq!( diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 8116a8b973..a8e630c1a3 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1817,14 +1817,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 @@ -1947,83 +1947,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(); + let metadata = futures::future::join_all( + events + .iter() + .map(|event| self.fs.metadata(&event.path)) + .collect::>(), + ) + .await; - events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); - events.dedup_by(|a, b| a.path.starts_with(&b.path)); - - 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() { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 001667d311..90d0e30b65 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -913,7 +913,7 @@ mod tests { ); } - #[gpui::test] + #[gpui::test(iterations = 30)] async fn test_editing_files(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); From 657ea264cc9c213eee5fa29e1b008ceee0d9f379 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 May 2022 17:55:33 -0700 Subject: [PATCH 09/26] Allow guests to create files from the project panel Co-authored-by: Nathan Sobo --- Cargo.lock | 4 +- crates/collab/src/rpc.rs | 68 +++++++++ crates/gpui/src/executor.rs | 16 ++- crates/project/Cargo.toml | 2 +- crates/project/src/project.rs | 72 ++++++++++ crates/project/src/worktree.rs | 162 +++++++++++++--------- crates/project_panel/src/project_panel.rs | 21 ++- crates/rpc/proto/zed.proto | 125 ++++++++++------- crates/rpc/src/proto.rs | 8 ++ 9 files changed, 344 insertions(+), 134 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d74f09da1d..77dd2908ba 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" diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 1813d8f52d..faf1da297d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -126,6 +126,7 @@ 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::update_buffer) .add_message_handler(Server::update_buffer_file) .add_message_handler(Server::buffer_reloaded) @@ -1808,6 +1809,73 @@ mod tests { .await; } + #[gpui::test(iterations = 10)] + async fn test_worktree_manipulation( + 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()); + + project_b + .update(cx_b, |project, cx| { + project.create_file((worktree_id, "c.txt"), cx).unwrap() + }) + .await + .unwrap(); + + executor.run_until_parked(); + 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"] + ); + }); + } + #[gpui::test(iterations = 10)] async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 2c80e01d6d..3126e18c7a 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/project/Cargo.toml b/crates/project/Cargo.toml index 728dae3128..eaae45bcc6 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/project.rs b/crates/project/src/project.rs index 97697c26f2..307ef16d3a 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::{ @@ -259,6 +261,7 @@ 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_apply_additional_edits_for_completion); client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_reload_buffers); @@ -686,6 +689,47 @@ impl Project { .map(|worktree| worktree.read(cx).id()) } + pub fn create_file( + &mut self, + project_path: impl Into, + 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().write_file( + project_path.path, + Default::default(), + 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: false, + }) + .await?; + worktree.update(&mut cx, |worktree, _| { + let worktree = worktree.as_remote_mut().unwrap(); + worktree.snapshot.insert_entry( + response + .entry + .ok_or_else(|| anyhow!("missing entry in response"))?, + ) + }) + })) + } + } + pub fn can_share(&self, cx: &AppContext) -> bool { self.is_local() && self.visible_worktrees(cx).next().is_some() } @@ -3733,6 +3777,34 @@ impl Project { }) } + async fn handle_create_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry = this + .update(&mut cx, |this, cx| { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let worktree = this + .worktree_for_id(worktree_id, cx) + .ok_or_else(|| anyhow!("worktree not found"))?; + worktree.update(cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + if envelope.payload.is_directory { + unimplemented!("can't yet create directories"); + } else { + let path = PathBuf::from(OsString::from_vec(envelope.payload.path)); + anyhow::Ok(worktree.write_file(path, Default::default(), cx)) + } + }) + })? + .await?; + Ok(proto::CreateProjectEntryResponse { + entry: Some((&entry).into()), + }) + } + async fn handle_update_diagnostic_summary( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index a8e630c1a3..d4d6160a4d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -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}, @@ -623,13 +624,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, None).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 { @@ -653,7 +656,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?; @@ -673,7 +676,7 @@ impl LocalWorktree { }) } - pub fn save( + pub fn write_file( &self, path: impl Into>, text: Rope, @@ -681,22 +684,21 @@ impl LocalWorktree { ) -> 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, - None, - ) - .await + let save = cx.background().spawn({ + let fs = self.fs.clone(); + let abs_path = abs_path.clone(); + async move { fs.save(&abs_path, &text).await } }); cx.spawn(|this, mut cx| async move { - let entry = save.await?; + save.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)); Ok(entry) }) @@ -712,28 +714,68 @@ impl LocalWorktree { let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); - let background_snapshot = self.background_snapshot.clone(); - let fs = self.fs.clone(); - let rename = cx.background().spawn(async move { - fs.rename(&abs_old_path, &abs_new_path, Default::default()) - .await?; - refresh_entry( - fs.as_ref(), - &background_snapshot, - new_path.clone(), - &abs_new_path, - Some(old_path), - ) - .await + 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 + } }); cx.spawn(|this, mut cx| async move { - let entry = rename.await?; + 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)); 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, @@ -914,6 +956,21 @@ 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))?; + self.entries_by_id.insert_or_replace( + PathEntry { + id: entry.id, + path: entry.path.clone(), + is_ignored: entry.is_ignored, + scan_id: 0, + }, + &(), + ); + self.entries_by_path.insert_or_replace(entry.clone(), &()); + Ok(entry) + } + 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(); @@ -1437,7 +1494,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 { @@ -2155,35 +2212,6 @@ impl BackgroundScanner { } } -async fn refresh_entry( - fs: &dyn Fs, - snapshot: &Mutex, - path: Arc, - abs_path: &Path, - old_path: Option>, -) -> 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, - ); - let mut snapshot = snapshot.lock(); - if let Some(old_path) = old_path { - snapshot.remove_path(&old_path); - } - Ok(snapshot.insert_entry(entry, fs)) -} - fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { let mut result = root_char_bag; result.extend( @@ -2421,7 +2449,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, @@ -2439,10 +2467,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, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 90d0e30b65..0ee26d9c39 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -273,27 +273,19 @@ impl ProjectPanel { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { let edit_state = self.edit_state.take()?; cx.focus_self(); + let worktree = self .project .read(cx) .worktree_for_id(edit_state.worktree_id, cx)?; - - // TODO - implement this for remote projects - if !worktree.read(cx).is_local() { - return None; - } - let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?; let filename = self.filename_editor.read(cx).text(cx); if edit_state.new_file { let new_path = entry.path.join(filename); - let save = worktree.update(cx, |worktree, cx| { - worktree - .as_local() - .unwrap() - .save(new_path, Default::default(), cx) - }); + let save = self.project.update(cx, |project, cx| { + project.create_file((edit_state.worktree_id, new_path), cx) + })?; Some(cx.spawn(|this, mut cx| async move { let new_entry = save.await?; this.update(&mut cx, |this, cx| { @@ -303,6 +295,11 @@ impl ProjectPanel { Ok(()) })) } else { + // TODO - implement this for remote projects + if !worktree.read(cx).is_local() { + return None; + } + let old_path = entry.path.clone(); let new_path = if let Some(parent) = old_path.parent() { parent.join(filename) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index bf18db9e2b..3fc350e9b6 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; + CreateProjectEntry create_project_entry = 32; + CreateProjectEntryResponse create_project_entry_response = 33; + RenameProjectEntry rename_project_entry = 34; + DeleteProjectEntry delete_project_entry = 35; - 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; + UpdateDiagnosticSummary update_diagnostic_summary = 36; + StartLanguageServer start_language_server = 37; + UpdateLanguageServer update_language_server = 38; - UpdateContacts update_contacts = 71; + 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; - GetUsers get_users = 72; - GetUsersResponse get_users_response = 73; + 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; - Follow follow = 74; - FollowResponse follow_response = 75; - UpdateFollowers update_followers = 76; - Unfollow unfollow = 77; + 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; } } @@ -158,6 +164,31 @@ message UpdateWorktree { repeated uint64 removed_entries = 5; } +message CreateProjectEntry { + uint64 project_id = 1; + uint64 worktree_id = 2; + bytes path = 3; + bool is_directory = 4; +} + +message CreateProjectEntryResponse { + Entry entry = 1; +} + +message RenameProjectEntry { + uint64 project_id = 1; + uint64 old_worktree_id = 2; + string old_path = 3; + uint64 new_worktree_id = 4; + string new_path = 5; +} + +message DeleteProjectEntry { + uint64 project_id = 1; + uint64 worktree_id = 2; + string path = 3; +} + message AddProjectCollaborator { uint64 project_id = 1; Collaborator collaborator = 2; @@ -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 98fc493774..a1c28075f5 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -147,6 +147,9 @@ messages!( (BufferReloaded, Foreground), (BufferSaved, Foreground), (ChannelMessageSent, Foreground), + (CreateProjectEntry, Foreground), + (CreateProjectEntryResponse, Foreground), + (DeleteProjectEntry, Foreground), (Error, Foreground), (Follow, Foreground), (FollowResponse, Foreground), @@ -194,6 +197,7 @@ messages!( (ReloadBuffers, Foreground), (ReloadBuffersResponse, Foreground), (RemoveProjectCollaborator, Foreground), + (RenameProjectEntry, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), @@ -219,6 +223,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), + (CreateProjectEntry, CreateProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), @@ -257,6 +262,9 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, + CreateProjectEntry, + RenameProjectEntry, + DeleteProjectEntry, Follow, FormatBuffers, GetCodeActions, From 470d693d5e1e112b20f0abb815b5e0d599d71d3e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 May 2022 09:11:59 -0600 Subject: [PATCH 10/26] Rename entries via the project to prepare for guest support Co-Authored-By: Antonio Scandurra --- crates/project/src/project.rs | 20 ++++++++++++++++++++ crates/project/src/worktree.rs | 12 ++++++------ crates/project_panel/src/project_panel.rs | 10 ++++++---- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 307ef16d3a..ea95a6e91d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -730,6 +730,26 @@ impl Project { } } + pub fn rename_entry( + &mut self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + if self.is_local() { + let worktree = self.worktree_for_entry(entry_id, cx)?; + + worktree.update(cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .rename_entry(entry_id, new_path, cx) + }) + } else { + todo!() + } + } + pub fn can_share(&self, cx: &AppContext) -> bool { self.is_local() && self.visible_worktrees(cx).next().is_some() } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index d4d6160a4d..65d5871bb6 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -704,13 +704,13 @@ impl LocalWorktree { }) } - pub fn rename( + pub fn rename_entry( &self, - old_path: impl Into>, + entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Task> { - let old_path = old_path.into(); + ) -> 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); @@ -723,7 +723,7 @@ impl LocalWorktree { } }); - cx.spawn(|this, mut cx| async move { + Some(cx.spawn(|this, mut cx| async move { rename.await?; let entry = this .update(&mut cx, |this, _| { @@ -736,7 +736,7 @@ impl LocalWorktree { .await?; this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); Ok(entry) - }) + })) } fn refresh_entry( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 0ee26d9c39..dba6ce1037 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -278,7 +278,7 @@ impl ProjectPanel { .project .read(cx) .worktree_for_id(edit_state.worktree_id, cx)?; - let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?; + let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone(); let filename = self.filename_editor.read(cx).text(cx); if edit_state.new_file { @@ -306,9 +306,11 @@ impl ProjectPanel { } else { filename.into() }; - let rename = worktree.update(cx, |worktree, cx| { - worktree.as_local().unwrap().rename(old_path, new_path, cx) - }); + + let rename = self.project.update(cx, |project, cx| { + project.rename_entry(entry.id, new_path, cx) + })?; + Some(cx.spawn(|this, mut cx| async move { let new_entry = rename.await?; this.update(&mut cx, |this, cx| { From 438e4e7a19b5f26235d1e79a768829278e8c3177 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 May 2022 10:27:04 -0700 Subject: [PATCH 11/26] Allow guests to rename stuff Co-Authored-By: Antonio Scandurra Co-Authored-By: Nathan Sobo --- crates/collab/src/rpc.rs | 33 +++++++++++++-- crates/project/src/project.rs | 50 ++++++++++++++++++++--- crates/project/src/worktree.rs | 8 ++++ crates/project_panel/src/project_panel.rs | 5 --- crates/rpc/proto/zed.proto | 20 ++++----- crates/rpc/src/proto.rs | 9 ++-- crates/sum_tree/src/sum_tree.rs | 17 ++++++++ 7 files changed, 113 insertions(+), 29 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index faf1da297d..cfea7cbc16 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -127,6 +127,7 @@ 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::update_buffer) .add_message_handler(Server::update_buffer_file) .add_message_handler(Server::buffer_reloaded) @@ -1810,7 +1811,7 @@ mod tests { } #[gpui::test(iterations = 10)] - async fn test_worktree_manipulation( + async fn test_fs_operations( executor: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -1848,14 +1849,12 @@ mod tests { let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); - project_b + let entry = project_b .update(cx_b, |project, cx| { project.create_file((worktree_id, "c.txt"), cx).unwrap() }) .await .unwrap(); - - executor.run_until_parked(); worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( worktree @@ -1874,6 +1873,32 @@ mod tests { [".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"] + ); + }); } #[gpui::test(iterations = 10)] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ea95a6e91d..7d1aacdde9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -262,6 +262,7 @@ impl Project { 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_apply_additional_edits_for_completion); client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_reload_buffers); @@ -736,9 +737,9 @@ impl Project { 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() { - let worktree = self.worktree_for_entry(entry_id, cx)?; - worktree.update(cx, |worktree, cx| { worktree .as_local_mut() @@ -746,7 +747,27 @@ impl Project { .rename_entry(entry_id, new_path, cx) }) } else { - todo!() + 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?; + worktree.update(&mut cx, |worktree, _| { + let worktree = worktree.as_remote_mut().unwrap(); + worktree.snapshot.remove_entry(entry_id); + worktree.snapshot.insert_entry( + response + .entry + .ok_or_else(|| anyhow!("missing entry in response"))?, + ) + }) + })) } } @@ -3802,7 +3823,7 @@ impl Project { envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, - ) -> Result { + ) -> Result { let entry = this .update(&mut cx, |this, cx| { let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); @@ -3820,7 +3841,26 @@ impl Project { }) })? .await?; - Ok(proto::CreateProjectEntryResponse { + Ok(proto::ProjectEntryResponse { + entry: Some((&entry).into()), + }) + } + + async fn handle_rename_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry = this + .update(&mut cx, |this, cx| { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path)); + this.rename_entry(entry_id, new_path, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + Ok(proto::ProjectEntryResponse { entry: Some((&entry).into()), }) } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 65d5871bb6..7b3c700911 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -956,6 +956,14 @@ impl Snapshot { self.entries_by_id.get(&entry_id, &()).is_some() } + pub(crate) fn remove_entry(&mut self, entry_id: ProjectEntryId) -> Option { + if let Some(entry) = self.entries_by_id.remove(&entry_id, &()) { + self.entries_by_path.remove(&PathKey(entry.path), &()) + } else { + None + } + } + pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result { let entry = Entry::try_from((&self.root_char_bag, entry))?; self.entries_by_id.insert_or_replace( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index dba6ce1037..5c96a254e5 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -295,11 +295,6 @@ impl ProjectPanel { Ok(()) })) } else { - // TODO - implement this for remote projects - if !worktree.read(cx).is_local() { - return None; - } - let old_path = entry.path.clone(); let new_path = if let Some(parent) = old_path.parent() { parent.join(filename) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3fc350e9b6..ffa2443537 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -38,9 +38,9 @@ message Envelope { UpdateWorktree update_worktree = 31; CreateProjectEntry create_project_entry = 32; - CreateProjectEntryResponse create_project_entry_response = 33; - RenameProjectEntry rename_project_entry = 34; - DeleteProjectEntry delete_project_entry = 35; + 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; @@ -171,16 +171,10 @@ message CreateProjectEntry { bool is_directory = 4; } -message CreateProjectEntryResponse { - Entry entry = 1; -} - message RenameProjectEntry { uint64 project_id = 1; - uint64 old_worktree_id = 2; - string old_path = 3; - uint64 new_worktree_id = 4; - string new_path = 5; + uint64 entry_id = 2; + bytes new_path = 3; } message DeleteProjectEntry { @@ -189,6 +183,10 @@ message DeleteProjectEntry { string path = 3; } +message ProjectEntryResponse { + Entry entry = 1; +} + message AddProjectCollaborator { uint64 project_id = 1; Collaborator collaborator = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index a1c28075f5..b2bcaa7d5f 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -148,7 +148,6 @@ messages!( (BufferSaved, Foreground), (ChannelMessageSent, Foreground), (CreateProjectEntry, Foreground), - (CreateProjectEntryResponse, Foreground), (DeleteProjectEntry, Foreground), (Error, Foreground), (Follow, Foreground), @@ -177,8 +176,6 @@ messages!( (JoinChannelResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), - (StartLanguageServer, Foreground), - (UpdateLanguageServer, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), (OpenBufferById, Background), @@ -190,6 +187,7 @@ messages!( (PerformRenameResponse, Background), (PrepareRename, Background), (PrepareRenameResponse, Background), + (ProjectEntryResponse, Foreground), (RegisterProjectResponse, Foreground), (Ping, Foreground), (RegisterProject, Foreground), @@ -204,6 +202,7 @@ messages!( (SendChannelMessage, Foreground), (SendChannelMessageResponse, Foreground), (ShareProject, Foreground), + (StartLanguageServer, Foreground), (Test, Foreground), (Unfollow, Foreground), (UnregisterProject, Foreground), @@ -214,6 +213,7 @@ messages!( (UpdateContacts, Foreground), (UpdateDiagnosticSummary, Foreground), (UpdateFollowers, Foreground), + (UpdateLanguageServer, Foreground), (UpdateWorktree, Foreground), ); @@ -223,7 +223,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), - (CreateProjectEntry, CreateProjectEntryResponse), + (CreateProjectEntry, ProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), @@ -246,6 +246,7 @@ request_messages!( (RegisterProject, RegisterProjectResponse), (RegisterWorktree, Ack), (ReloadBuffers, ReloadBuffersResponse), + (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), (SendChannelMessage, SendChannelMessageResponse), diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index c77b10e1bd..d524735bac 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -502,6 +502,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>, From ff3cf3c0c36fa988156e55f3c0755dcd62a331fb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 May 2022 10:33:26 -0700 Subject: [PATCH 12/26] Bump protocol version number --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index e0f6a0133c..ffddcb9cd3 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; From 821dff0e2df217a4314351f00b51a6f090bf8baf Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 May 2022 13:03:50 -0700 Subject: [PATCH 13/26] Keep showing edited filename in project panel while edit is in-progress --- crates/project_panel/src/project_panel.rs | 187 +++++++++++++--------- 1 file changed, 111 insertions(+), 76 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5c96a254e5..b76166886a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -44,11 +44,12 @@ struct Selection { entry_id: ProjectEntryId, } -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] struct EditState { worktree_id: WorktreeId, entry_id: ProjectEntryId, new_file: bool, + processing_filename: Option, } #[derive(Debug, PartialEq, Eq)] @@ -59,6 +60,7 @@ struct EntryDetails { is_expanded: bool, is_selected: bool, is_editing: bool, + is_processing: bool, } #[derive(Clone)] @@ -127,12 +129,6 @@ impl ProjectPanel { cx, ) }); - cx.subscribe(&filename_editor, |this, _, event, cx| { - if let editor::Event::Blurred = event { - this.editor_blurred(cx); - } - }) - .detach(); let mut this = Self { project: project.clone(), @@ -271,50 +267,57 @@ impl ProjectPanel { } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { - let edit_state = self.edit_state.take()?; + let edit_state = self.edit_state.as_mut()?; cx.focus_self(); - let worktree = self - .project - .read(cx) - .worktree_for_id(edit_state.worktree_id, cx)?; + 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.new_file { - let new_path = entry.path.join(filename); - let save = self.project.update(cx, |project, cx| { + self.selection = Some(Selection { + worktree_id, + entry_id: NEW_FILE_ENTRY_ID, + }); + let new_path = entry.path.join(&filename); + edited_entry_id = NEW_FILE_ENTRY_ID; + edit_task = self.project.update(cx, |project, cx| { project.create_file((edit_state.worktree_id, new_path), cx) })?; - Some(cx.spawn(|this, mut cx| async move { - let new_entry = save.await?; - this.update(&mut cx, |this, cx| { - this.update_visible_entries(Some((edit_state.worktree_id, new_entry.id)), cx); - cx.notify(); - }); - Ok(()) - })) } else { - let old_path = entry.path.clone(); - let new_path = if let Some(parent) = old_path.parent() { - parent.join(filename) + let new_path = if let Some(parent) = entry.path.clone().parent() { + parent.join(&filename) } else { - filename.into() + filename.clone().into() }; - - let rename = self.project.update(cx, |project, cx| { + edited_entry_id = entry.id; + edit_task = self.project.update(cx, |project, cx| { project.rename_entry(entry.id, new_path, cx) })?; + }; - Some(cx.spawn(|this, mut cx| async move { - let new_entry = rename.await?; - this.update(&mut cx, |this, cx| { - this.update_visible_entries(Some((edit_state.worktree_id, new_entry.id)), cx); - cx.notify(); - }); - Ok(()) - })) - } + 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(); + 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) { @@ -371,6 +374,7 @@ impl ProjectPanel { worktree_id, entry_id: directory_id, new_file: true, + processing_filename: None, }); self.filename_editor .update(cx, |editor, cx| editor.clear(cx)); @@ -392,6 +396,7 @@ impl ProjectPanel { worktree_id, entry_id, new_file: false, + processing_filename: None, }); let filename = entry .path @@ -409,13 +414,6 @@ impl ProjectPanel { } } - fn editor_blurred(&mut self, cx: &mut ViewContext) { - self.edit_state = None; - self.update_visible_entries(None, cx); - cx.focus_self(); - cx.notify(); - } - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { if let Some(selection) = self.selection { let (mut worktree_ix, mut entry_ix, _) = @@ -528,7 +526,7 @@ impl ProjectPanel { } }; - let new_file_parent_id = self.edit_state.and_then(|edit_state| { + let new_file_parent_id = self.edit_state.as_ref().and_then(|edit_state| { if edit_state.worktree_id == worktree_id && edit_state.new_file { Some(edit_state.entry_id) } else { @@ -668,19 +666,28 @@ impl ProjectPanel { e.worktree_id == snapshot.id() && e.entry_id == entry.id }), is_editing: false, + is_processing: false, }; - if let Some(edit_state) = self.edit_state { - if edit_state.new_file { - if entry.id == NEW_FILE_ENTRY_ID { - details.is_editing = true; - details.filename.clear(); - } + if let Some(edit_state) = &self.edit_state { + let is_edited_entry = if edit_state.new_file { + entry.id == NEW_FILE_ENTRY_ID } else { - if entry.id == edit_state.entry_id { + 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.new_file { + details.filename.clear(); + } details.is_editing = true; } - }; + } } + callback(entry.id, details, cx); } } @@ -696,10 +703,11 @@ impl ProjectPanel { cx: &mut ViewContext, ) -> ElementBox { let kind = details.kind; + let show_editor = details.is_editing && !details.is_processing; MouseEventHandler::new::(entry_id.to_usize(), cx, |state, _| { 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 details.is_editing { + let row_container_style = if show_editor { theme.filename_editor.container } else { style.container @@ -726,7 +734,7 @@ impl ProjectPanel { .with_width(style.icon_size) .boxed(), ) - .with_child(if details.is_editing { + .with_child(if show_editor { ChildView::new(editor.clone()) .contained() .with_margin_left(theme.entry.default.icon_spacing) @@ -987,15 +995,28 @@ mod tests { ] ); - panel - .update(cx, |panel, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); - panel.confirm(&Confirm, cx).unwrap() - }) - .await - .unwrap(); + 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), &[ @@ -1069,15 +1090,28 @@ mod tests { ] ); - panel - .update(cx, |panel, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("a-different-filename", cx)); - panel.confirm(&Confirm, cx).unwrap() - }) - .await - .unwrap(); + 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), &[ @@ -1167,11 +1201,12 @@ mod tests { } else { " " }; - let editor_text = format!("[EDITOR: '{}']", details.filename); let name = if details.is_editing { - &editor_text + format!("[EDITOR: '{}']", details.filename) + } else if details.is_processing { + format!("[PROCESSING: '{}']", details.filename) } else { - &details.filename + details.filename.clone() }; let selected = if details.is_selected { " <== selected" From a2c22a5e4399454bb2102931066ef8fb32eee3c9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 May 2022 15:10:39 -0700 Subject: [PATCH 14/26] Prevent eager snapshot mutations from being clobbered by background updates Co-authored-by: Nathan Sobo --- crates/collab/src/rpc.rs | 4 +- crates/project/src/project.rs | 33 ++++++------ crates/project/src/worktree.rs | 95 +++++++++++++++++++++++---------- crates/rpc/src/proto.rs | 3 +- crates/sum_tree/src/sum_tree.rs | 19 ++++--- 5 files changed, 96 insertions(+), 58 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index cfea7cbc16..8473e28c66 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -159,9 +159,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 { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7d1aacdde9..26a9ed14d7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -719,14 +719,14 @@ impl Project { is_directory: false, }) .await?; - worktree.update(&mut cx, |worktree, _| { - let worktree = worktree.as_remote_mut().unwrap(); - worktree.snapshot.insert_entry( - response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?, - ) - }) + 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, cx) + }) + .await })) } } @@ -758,15 +758,14 @@ impl Project { new_path: new_path.as_os_str().as_bytes().to_vec(), }) .await?; - worktree.update(&mut cx, |worktree, _| { - let worktree = worktree.as_remote_mut().unwrap(); - worktree.snapshot.remove_entry(entry_id); - worktree.snapshot.insert_entry( - response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?, - ) - }) + 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, cx) + }) + .await })) } } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 7b3c700911..2efeea1645 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -29,6 +29,7 @@ use language::{ use lazy_static::lazy_static; use parking_lot::Mutex; use postage::{ + barrier, prelude::{Sink as _, Stream as _}, watch, }; @@ -79,16 +80,21 @@ 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, + updates_tx: UnboundedSender, replica_id: ReplicaId, diagnostic_summaries: TreeMap, visible: bool, } +enum BackgroundUpdate { + Update(proto::UpdateWorktree), + Barrier(barrier::Sender), +} + #[derive(Clone)] pub struct Snapshot { id: WorktreeId, @@ -218,13 +224,14 @@ impl Worktree { }; 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 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, client: client.clone(), diagnostic_summaries: TreeMap::from_ordered_entries( @@ -275,37 +282,40 @@ 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) { - log::error!("error applying worktree update: {}", error); + if let BackgroundUpdate::Update(update) = update { + if let Err(error) = + background_snapshot.lock().apply_remote_update(update) + { + log::error!("error applying worktree update: {}", error); + } + snapshot_updated_tx.send(()).await.ok(); } - *snapshot_tx.borrow_mut() = snapshot; } }) .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)); } else { break; } } - }) - .detach(); - } + } + }) + .detach(); } }); (worktree_handle, deserialize_task) @@ -411,7 +421,7 @@ impl Worktree { } } Self::Remote(worktree) => { - worktree.snapshot = worktree.snapshot_rx.borrow().clone(); + worktree.snapshot = worktree.background_snapshot.lock().clone(); cx.emit(Event::UpdatedEntries); } }; @@ -923,12 +933,21 @@ impl RemoteWorktree { envelope: TypedEnvelope, ) -> Result<()> { self.updates_tx - .unbounded_send(envelope.payload) + .unbounded_send(BackgroundUpdate::Update(envelope.payload)) .expect("consumer runs to completion"); - Ok(()) } + pub fn finish_pending_remote_updates(&self) -> impl Future { + let (tx, mut rx) = barrier::channel(); + self.updates_tx + .unbounded_send(BackgroundUpdate::Barrier(tx)) + .expect("consumer runs to completion"); + async move { + rx.recv().await; + } + } + pub fn update_diagnostic_summary( &mut self, path: Arc, @@ -945,6 +964,29 @@ impl RemoteWorktree { .insert(PathKey(path.clone()), summary); } } + + pub fn insert_entry( + &self, + entry: proto::Entry, + cx: &mut ModelContext, + ) -> Task> { + cx.spawn(|this, mut cx| async move { + this.update(&mut cx, |worktree, _| { + worktree + .as_remote_mut() + .unwrap() + .finish_pending_remote_updates() + }) + .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 + }) + }) + } } impl Snapshot { @@ -956,17 +998,9 @@ impl Snapshot { self.entries_by_id.get(&entry_id, &()).is_some() } - pub(crate) fn remove_entry(&mut self, entry_id: ProjectEntryId) -> Option { - if let Some(entry) = self.entries_by_id.remove(&entry_id, &()) { - self.entries_by_path.remove(&PathKey(entry.path), &()) - } else { - None - } - } - pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result { let entry = Entry::try_from((&self.root_char_bag, entry))?; - self.entries_by_id.insert_or_replace( + let old_entry = self.entries_by_id.insert_or_replace( PathEntry { id: entry.id, path: entry.path.clone(), @@ -975,6 +1009,9 @@ impl Snapshot { }, &(), ); + 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) } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index b2bcaa7d5f..59053997d3 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( diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index d524735bac..193786112b 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); From 40e0f101958c4085bcefb3928f448d0d72d958ad Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 May 2022 16:47:11 -0700 Subject: [PATCH 15/26] Allow creating directories from the project panel --- crates/collab/src/rpc.rs | 4 +- crates/project/src/project.rs | 16 ++- crates/project/src/worktree.rs | 72 ++++++++---- crates/project_panel/src/project_panel.rs | 131 +++++++++++++++++----- 4 files changed, 166 insertions(+), 57 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8473e28c66..a429a16a47 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1849,7 +1849,9 @@ mod tests { let entry = project_b .update(cx_b, |project, cx| { - project.create_file((worktree_id, "c.txt"), cx).unwrap() + project + .create_entry((worktree_id, "c.txt"), false, cx) + .unwrap() }) .await .unwrap(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 26a9ed14d7..d0891cadb1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -690,33 +690,31 @@ impl Project { .map(|worktree| worktree.read(cx).id()) } - pub fn create_file( + 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().write_file( - project_path.path, - Default::default(), - 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: false, + is_directory, }) .await?; let entry = response diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 2efeea1645..db4769b7aa 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -686,32 +686,30 @@ impl LocalWorktree { }) } + 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> { - let path = path.into(); - let abs_path = self.absolutize(&path); - let save = cx.background().spawn({ - let fs = self.fs.clone(); - let abs_path = abs_path.clone(); - async move { fs.save(&abs_path, &text).await } - }); - - cx.spawn(|this, mut cx| async move { - save.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)); - Ok(entry) - }) + self.write_entry_internal(path, Some(text), cx) } pub fn rename_entry( @@ -749,6 +747,40 @@ impl LocalWorktree { })) } + 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 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 { + 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)); + Ok(entry) + }) + } + fn refresh_entry( &self, path: Arc, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b76166886a..18ff22cd95 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -25,7 +25,7 @@ use workspace::{ Workspace, }; -const NEW_FILE_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; +const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; pub struct ProjectPanel { project: ModelHandle, @@ -48,7 +48,8 @@ struct Selection { struct EditState { worktree_id: WorktreeId, entry_id: ProjectEntryId, - new_file: bool, + is_new_entry: bool, + is_dir: bool, processing_filename: Option, } @@ -71,7 +72,13 @@ pub struct Open(pub ProjectEntryId); actions!( project_panel, - [ExpandSelectedEntry, CollapseSelectedEntry, AddFile, Rename] + [ + ExpandSelectedEntry, + CollapseSelectedEntry, + AddDirectory, + AddFile, + Rename + ] ); impl_internal_actions!(project_panel, [Open, ToggleExpanded]); @@ -83,6 +90,7 @@ pub fn init(cx: &mut MutableAppContext) { 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::confirm); cx.add_action(ProjectPanel::cancel); @@ -278,15 +286,15 @@ impl ProjectPanel { let edit_task; let edited_entry_id; - if edit_state.new_file { + if edit_state.is_new_entry { self.selection = Some(Selection { worktree_id, - entry_id: NEW_FILE_ENTRY_ID, + entry_id: NEW_ENTRY_ID, }); let new_path = entry.path.join(&filename); - edited_entry_id = NEW_FILE_ENTRY_ID; + edited_entry_id = NEW_ENTRY_ID; edit_task = self.project.update(cx, |project, cx| { - project.create_file((edit_state.worktree_id, new_path), 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() { @@ -332,6 +340,14 @@ impl ProjectPanel { } 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, @@ -373,13 +389,14 @@ impl ProjectPanel { self.edit_state = Some(EditState { worktree_id, entry_id: directory_id, - new_file: true, + 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(None, cx); + self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx); cx.notify(); } } @@ -395,7 +412,8 @@ impl ProjectPanel { self.edit_state = Some(EditState { worktree_id, entry_id, - new_file: false, + is_new_entry: false, + is_dir: entry.is_dir(), processing_filename: None, }); let filename = entry @@ -526,22 +544,27 @@ impl ProjectPanel { } }; - let new_file_parent_id = self.edit_state.as_ref().and_then(|edit_state| { - if edit_state.worktree_id == worktree_id && edit_state.new_file { - Some(edit_state.entry_id) - } else { - None + 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(entry) = entry_iter.entry() { visible_worktree_entries.push(entry.clone()); - if Some(entry.id) == new_file_parent_id { + if Some(entry.id) == new_entry_parent_id { visible_worktree_entries.push(Entry { - id: NEW_FILE_ENTRY_ID, - kind: project::EntryKind::File(Default::default()), + id: NEW_ENTRY_ID, + kind: new_entry_kind, path: entry.path.join("\0").into(), inode: 0, mtime: entry.mtime, @@ -669,8 +692,8 @@ impl ProjectPanel { is_processing: false, }; if let Some(edit_state) = &self.edit_state { - let is_edited_entry = if edit_state.new_file { - entry.id == NEW_FILE_ENTRY_ID + let is_edited_entry = if edit_state.is_new_entry { + entry.id == NEW_ENTRY_ID } else { entry.id == edit_state.entry_id }; @@ -680,7 +703,7 @@ impl ProjectPanel { details.filename.clear(); details.filename.push_str(&processing_filename); } else { - if edit_state.new_file { + if edit_state.is_new_entry { details.filename.clear(); } details.is_editing = true; @@ -983,11 +1006,11 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ - "v root1 <== selected", + "v root1", " > a", " > b", " > C", - " [EDITOR: '']", + " [EDITOR: ''] <== selected", " .dockerignore", "v root2", " > d", @@ -1039,10 +1062,10 @@ mod tests { &[ "v root1", " > a", - " v b <== selected", + " v b", " > 3", " > 4", - " [EDITOR: '']", + " [EDITOR: ''] <== selected", " > C", " .dockerignore", " the-new-filename", @@ -1126,6 +1149,60 @@ mod tests { " 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( @@ -1192,7 +1269,7 @@ mod tests { } let indent = " ".repeat(details.depth); - let icon = if details.kind == EntryKind::Dir { + let icon = if matches!(details.kind, EntryKind::Dir | EntryKind::PendingDir) { if details.is_expanded { "v " } else { From 509ede0e80e81404e8ad76e621fef7ce72230b04 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 May 2022 16:52:46 -0700 Subject: [PATCH 16/26] Allow guests to create directories --- crates/collab/src/rpc.rs | 27 +++++++++++++++++++++++++++ crates/project/src/project.rs | 8 ++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index a429a16a47..624eba59a3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1899,6 +1899,33 @@ mod tests { [".zed.toml", "a.txt", "b.txt", "d.txt"] ); }); + + 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"] + ); + }); } #[gpui::test(iterations = 10)] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d0891cadb1..5590c61762 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3829,12 +3829,8 @@ impl Project { .ok_or_else(|| anyhow!("worktree not found"))?; worktree.update(cx, |worktree, cx| { let worktree = worktree.as_local_mut().unwrap(); - if envelope.payload.is_directory { - unimplemented!("can't yet create directories"); - } else { - let path = PathBuf::from(OsString::from_vec(envelope.payload.path)); - anyhow::Ok(worktree.write_file(path, Default::default(), cx)) - } + let path = PathBuf::from(OsString::from_vec(envelope.payload.path)); + anyhow::Ok(worktree.create_entry(path, envelope.payload.is_directory, cx)) }) })? .await?; From 4b1c46fa450d9eb9d2dd21c165a3fdb0a31e5a78 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 May 2022 17:53:29 -0700 Subject: [PATCH 17/26] Allow deleting entries from the project panel --- Cargo.lock | 2 + assets/keymaps/default.json | 3 +- crates/collab/src/rpc.rs | 53 ++++++++++++++++- crates/project/src/project.rs | 45 ++++++++++++++ crates/project/src/worktree.rs | 72 ++++++++++++++++++++++- crates/project_panel/Cargo.toml | 2 + crates/project_panel/src/project_panel.rs | 25 +++++++- crates/rpc/proto/zed.proto | 3 +- crates/rpc/src/proto.rs | 1 + 9 files changed, 198 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77dd2908ba..6d7ce98341 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3330,7 +3330,9 @@ 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 b7d390062f..dd83b91ed0 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -332,7 +332,8 @@ "bindings": { "left": "project_panel::CollapseSelectedEntry", "right": "project_panel::ExpandSelectedEntry", - "f2": "project_panel::Rename" + "f2": "project_panel::Rename", + "backspace": "project_panel::Delete" } } ] \ No newline at end of file diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 624eba59a3..2367532d28 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -128,6 +128,7 @@ 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::update_buffer) .add_message_handler(Server::update_buffer_file) .add_message_handler(Server::buffer_reloaded) @@ -1900,7 +1901,7 @@ mod tests { ); }); - project_b + let dir_entry = project_b .update(cx_b, |project, cx| { project .create_entry((worktree_id, "DIR"), true, cx) @@ -1926,6 +1927,56 @@ mod tests { [".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)] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5590c61762..7af1199ec1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -263,6 +263,7 @@ impl Project { 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); @@ -768,6 +769,35 @@ impl Project { } } + 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 { + 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, cx) + }) + .await + })) + } + } + pub fn can_share(&self, cx: &AppContext) -> bool { self.is_local() && self.visible_worktrees(cx).next().is_some() } @@ -3858,6 +3888,21 @@ impl Project { }) } + async fn handle_delete_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |this, cx| { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + this.delete_entry(entry_id, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + Ok(proto::Ack {}) + } + async fn handle_update_diagnostic_summary( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index db4769b7aa..2a1808457c 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}, @@ -712,6 +712,44 @@ impl LocalWorktree { 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)); + Ok(()) + })) + } + pub fn rename_entry( &self, entry_id: ProjectEntryId, @@ -1019,6 +1057,29 @@ impl RemoteWorktree { }) }) } + + pub(crate) fn delete_entry( + &self, + id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Task> { + cx.spawn(|this, mut cx| async move { + this.update(&mut cx, |worktree, _| { + worktree + .as_remote_mut() + .unwrap() + .finish_pending_remote_updates() + }) + .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 { @@ -1048,6 +1109,15 @@ impl Snapshot { 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(); diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 4b78f2a1fa..e431db45dd 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -15,6 +15,8 @@ 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] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 18ff22cd95..6bb4f640ee 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,15 +1,16 @@ use editor::{Cancel, Editor}; +use futures::stream::StreamExt; use gpui::{ actions, - anyhow::Result, + anyhow::{anyhow, Result}, elements::{ ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, Svg, UniformList, UniformListState, }, impl_internal_actions, keymap, platform::CursorStyle, - AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; @@ -77,6 +78,7 @@ actions!( CollapseSelectedEntry, AddDirectory, AddFile, + Delete, Rename ] ); @@ -92,6 +94,7 @@ pub fn init(cx: &mut MutableAppContext) { 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); } @@ -432,6 +435,22 @@ impl ProjectPanel { } } + fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { + let Selection { entry_id, .. } = self.selection?; + let mut answer = cx.prompt(PromptLevel::Info, "Delete?", &["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) { if let Some(selection) = self.selection { let (mut worktree_ix, mut entry_ix, _) = diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ffa2443537..8a30278920 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -179,8 +179,7 @@ message RenameProjectEntry { message DeleteProjectEntry { uint64 project_id = 1; - uint64 worktree_id = 2; - string path = 3; + uint64 entry_id = 2; } message ProjectEntryResponse { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 59053997d3..428eb13a42 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -225,6 +225,7 @@ request_messages!( ApplyCompletionAdditionalEditsResponse ), (CreateProjectEntry, ProjectEntryResponse), + (DeleteProjectEntry, Ack), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), From ecb847a027f6856db50fa36aafb204650d250339 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 May 2022 09:02:31 +0200 Subject: [PATCH 18/26] Fix bugs in `FakeFs::{remove_dir,rename}` --- crates/project/src/fs.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index bc815f5be1..912dc65afe 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -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(()) From 6212f2fe302d6478f50450094c590842697df096 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 May 2022 13:47:53 +0200 Subject: [PATCH 19/26] Wait for remote worktree to catch up with host before mutating entries This ensures that entries don't randomly re-appear on remote worktrees due to observing an update too late. In fact, it ensures that the remote worktree has the same starting state of the host before preemptively applying the fs operation locally. --- crates/collab/src/rpc.rs | 3 + crates/collab/src/rpc/store.rs | 3 + crates/project/src/project.rs | 91 ++++++++++++------ crates/project/src/worktree.rs | 163 ++++++++++++++++++++------------- crates/rpc/proto/zed.proto | 3 + crates/rpc/src/proto.rs | 2 +- 6 files changed, 171 insertions(+), 94 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 2367532d28..d17473e139 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -448,6 +448,7 @@ impl Server { .cloned() .collect(), visible: worktree.visible, + scan_id: shared_worktree.scan_id, }) }) .collect(); @@ -578,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| { @@ -5804,6 +5806,7 @@ mod tests { guest_client.username, id ); + assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id()); } guest_client diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 3be072c5e2..4737dd2c80 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/project/src/project.rs b/crates/project/src/project.rs index 7af1199ec1..1fcd89fcde 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -723,7 +723,11 @@ impl Project { .ok_or_else(|| anyhow!("missing entry in response"))?; worktree .update(&mut cx, |worktree, cx| { - worktree.as_remote().unwrap().insert_entry(entry, cx) + worktree.as_remote().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) }) .await })) @@ -762,7 +766,11 @@ impl Project { .ok_or_else(|| anyhow!("missing entry in response"))?; worktree .update(&mut cx, |worktree, cx| { - worktree.as_remote().unwrap().insert_entry(entry, cx) + worktree.as_remote().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) }) .await })) @@ -783,7 +791,7 @@ impl Project { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); Some(cx.spawn_weak(|_, mut cx| async move { - client + let response = client .request(proto::DeleteProjectEntry { project_id, entry_id: entry_id.to_proto(), @@ -791,7 +799,11 @@ impl Project { .await?; worktree .update(&mut cx, move |worktree, cx| { - worktree.as_remote().unwrap().delete_entry(entry_id, cx) + worktree.as_remote().unwrap().delete_entry( + entry_id, + response.worktree_scan_id as usize, + cx, + ) }) .await })) @@ -3805,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); @@ -3851,21 +3864,22 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let entry = this - .update(&mut cx, |this, cx| { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let worktree = this - .worktree_for_id(worktree_id, cx) - .ok_or_else(|| anyhow!("worktree not found"))?; - worktree.update(cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - let path = PathBuf::from(OsString::from_vec(envelope.payload.path)); - anyhow::Ok(worktree.create_entry(path, envelope.payload.is_directory, cx)) - }) - })? + 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, }) } @@ -3875,16 +3889,25 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let entry = this - .update(&mut cx, |this, cx| { - let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + 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)); - this.rename_entry(entry_id, new_path, cx) + 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, }) } @@ -3893,14 +3916,26 @@ impl Project { envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, - ) -> Result { - this.update(&mut cx, |this, cx| { - let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); - this.delete_entry(entry_id, cx) - .ok_or_else(|| anyhow!("invalid entry")) - })? - .await?; - Ok(proto::Ack {}) + ) -> 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( diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 2a1808457c..bab41bbe27 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -29,7 +29,6 @@ use language::{ use lazy_static::lazy_static; use parking_lot::Mutex; use postage::{ - barrier, prelude::{Sink as _, Stream as _}, watch, }; @@ -84,17 +83,13 @@ pub struct RemoteWorktree { pub(crate) background_snapshot: Arc>, project_id: u64, client: Arc, - updates_tx: UnboundedSender, + updates_tx: UnboundedSender, + last_scan_id_rx: watch::Receiver, replica_id: ReplicaId, diagnostic_summaries: TreeMap, visible: bool, } -enum BackgroundUpdate { - Update(proto::UpdateWorktree), - Barrier(barrier::Sender), -} - #[derive(Clone)] pub struct Snapshot { id: WorktreeId, @@ -102,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, @@ -221,11 +216,13 @@ 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 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, @@ -233,6 +230,7 @@ impl Worktree { snapshot: snapshot.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| { @@ -291,14 +289,12 @@ impl Worktree { cx.background() .spawn(async move { while let Some(update) = updates_rx.next().await { - if let BackgroundUpdate::Update(update) = update { - if let Err(error) = - background_snapshot.lock().apply_remote_update(update) - { - log::error!("error applying worktree update: {}", error); - } - snapshot_updated_tx.send(()).await.ok(); + if let Err(error) = + background_snapshot.lock().apply_remote_update(update) + { + log::error!("error applying worktree update: {}", error); } + snapshot_updated_tx.send(()).await.ok(); } }) .detach(); @@ -308,7 +304,11 @@ impl Worktree { 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; } @@ -368,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, @@ -465,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, @@ -475,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 { @@ -505,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; } @@ -745,7 +741,11 @@ impl LocalWorktree { let mut snapshot = this.background_snapshot.lock(); snapshot.delete_entry(entry_id); }); - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + this.as_local().unwrap().broadcast_snapshot() + }) + .await; Ok(()) })) } @@ -780,7 +780,11 @@ impl LocalWorktree { ) }) .await?; - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + this.as_local().unwrap().broadcast_snapshot() + }) + .await; Ok(entry) })) } @@ -814,7 +818,11 @@ impl LocalWorktree { .refresh_entry(path, abs_path, None) }) .await?; - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + this.as_local().unwrap().broadcast_snapshot() + }) + .await; Ok(entry) }) } @@ -923,6 +931,7 @@ impl LocalWorktree { .map(Into::into) .collect(), removed_entries: Default::default(), + scan_id: snapshot.scan_id as u64, }) .await { @@ -991,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 { @@ -1003,18 +1029,19 @@ impl RemoteWorktree { envelope: TypedEnvelope, ) -> Result<()> { self.updates_tx - .unbounded_send(BackgroundUpdate::Update(envelope.payload)) + .unbounded_send(envelope.payload) .expect("consumer runs to completion"); Ok(()) } - pub fn finish_pending_remote_updates(&self) -> impl Future { - let (tx, mut rx) = barrier::channel(); - self.updates_tx - .unbounded_send(BackgroundUpdate::Barrier(tx)) - .expect("consumer runs to completion"); + fn wait_for_snapshot(&self, scan_id: usize) -> impl Future { + let mut rx = self.last_scan_id_rx.clone(); async move { - rx.recv().await; + while let Some(applied_scan_id) = rx.next().await { + if applied_scan_id >= scan_id { + return; + } + } } } @@ -1038,16 +1065,12 @@ impl RemoteWorktree { 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 { - this.update(&mut cx, |worktree, _| { - worktree - .as_remote_mut() - .unwrap() - .finish_pending_remote_updates() - }) - .await; + wait_for_snapshot.await; this.update(&mut cx, |worktree, _| { let worktree = worktree.as_remote_mut().unwrap(); let mut snapshot = worktree.background_snapshot.lock(); @@ -1061,16 +1084,12 @@ impl RemoteWorktree { 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 { - this.update(&mut cx, |worktree, _| { - worktree - .as_remote_mut() - .unwrap() - .finish_pending_remote_updates() - }) - .await; + wait_for_snapshot.await; this.update(&mut cx, |worktree, _| { let worktree = worktree.as_remote_mut().unwrap(); let mut snapshot = worktree.background_snapshot.lock(); @@ -1145,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(()) } @@ -1233,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) @@ -1282,6 +1306,7 @@ impl LocalSnapshot { .map(|(path, summary)| summary.to_proto(&path.0)) .collect(), visible, + scan_id: self.scan_id as u64, } } @@ -1347,6 +1372,7 @@ impl LocalSnapshot { root_name: self.root_name().to_string(), updated_entries, removed_entries, + scan_id: self.scan_id as u64, } } @@ -1390,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)); } @@ -1454,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; } } } @@ -2773,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(), @@ -2783,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/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8a30278920..fa0b587df4 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -162,6 +162,7 @@ message UpdateWorktree { string root_name = 3; repeated Entry updated_entries = 4; repeated uint64 removed_entries = 5; + uint64 scan_id = 6; } message CreateProjectEntry { @@ -184,6 +185,7 @@ message DeleteProjectEntry { message ProjectEntryResponse { Entry entry = 1; + uint64 worktree_scan_id = 2; } message AddProjectCollaborator { @@ -658,6 +660,7 @@ message Worktree { repeated Entry entries = 3; repeated DiagnosticSummary diagnostic_summaries = 4; bool visible = 5; + uint64 scan_id = 6; } message File { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 428eb13a42..c505869c55 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -225,7 +225,7 @@ request_messages!( ApplyCompletionAdditionalEditsResponse ), (CreateProjectEntry, ProjectEntryResponse), - (DeleteProjectEntry, Ack), + (DeleteProjectEntry, ProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), From 6b22c47d47b0970fd5b8016fd074f85dd035a83c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 May 2022 14:54:35 +0200 Subject: [PATCH 20/26] Introduce guest file creation in randomized collaboration test Co-Authored-By: Nathan Sobo --- crates/collab/src/rpc.rs | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index d17473e139..4a4985c470 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -6528,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!( From 61346f734d1fc2fc1b45e3a9609f8620cdee8bc1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 May 2022 15:15:58 +0200 Subject: [PATCH 21/26] WIP --- crates/auto_update/src/auto_update.rs | 2 +- crates/chat_panel/src/chat_panel.rs | 2 +- crates/contacts_panel/src/contacts_panel.rs | 2 +- crates/diagnostics/src/items.rs | 4 ++-- crates/editor/src/element.rs | 2 +- crates/gpui/src/elements/mouse_event_handler.rs | 12 ++++++++---- crates/gpui/src/platform/event.rs | 3 ++- crates/gpui/src/platform/mac/event.rs | 1 + crates/gpui/src/views/select.rs | 4 ++-- crates/project_panel/src/project_panel.rs | 14 ++++++++++---- crates/search/src/buffer_search.rs | 4 ++-- crates/search/src/project_search.rs | 4 ++-- crates/workspace/src/lsp_status.rs | 2 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/sidebar.rs | 2 +- crates/workspace/src/workspace.rs | 6 +++--- 16 files changed, 39 insertions(+), 27 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index f722167b6f..499b3ed99d 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 415ff6187e..bb835c6640 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/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 45b5f69b5e..171b419496 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 39b5437a6a..ef99cbf5a8 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/element.rs b/crates/editor/src/element.rs index f4453774f7..a04d27a8bb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1189,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/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 12166d45b5..1ee7c6cbb5 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/platform/event.rs b/crates/gpui/src/platform/event.rs index fe353fed4c..b32ab952c7 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 7170bd2fd5..651805370c 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 10cd0cd5a2..d5d2105c3f 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_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 6bb4f640ee..220278fd1d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -69,7 +69,10 @@ struct EntryDetails { 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, @@ -339,7 +342,7 @@ impl ProjectPanel { } fn open_entry(&mut self, action: &Open, cx: &mut ViewContext) { - cx.emit(Event::OpenedEntry(action.0)); + cx.emit(Event::OpenedEntry(action.entry_id)); } fn add_file(&mut self, _: &AddFile, cx: &mut ViewContext) { @@ -799,11 +802,14 @@ impl ProjectPanel { .with_padding_left(padding) .boxed() }) - .on_click(move |cx| { + .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) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d1f17608f2..549edf89e7 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 d3b3520a53..cbd373c468 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() } diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index ddc6d89308..f58e0b973e 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 a992897c11..94fe7c3a42 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -737,7 +737,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(), diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 2f13469cec..bc7314e732 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 d072c51550..fe18fcb95a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1584,7 +1584,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 +1635,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 +1667,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 { From 2e6cf2011d842dd89ba80b6dcc85c109b7e1d9ff Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 May 2022 15:27:27 +0200 Subject: [PATCH 22/26] When opening items via project panel, only focus them on double-click Co-Authored-By: Nathan Sobo --- crates/collab/src/rpc.rs | 22 +++++----- crates/file_finder/src/file_finder.rs | 2 +- crates/project_panel/src/project_panel.rs | 51 ++++++++++++++++------- crates/vim/src/vim_test_context.rs | 2 +- crates/workspace/src/pane.rs | 47 ++++++++++++++------- crates/workspace/src/workspace.rs | 14 ++++--- crates/zed/src/zed.rs | 21 +++++----- 7 files changed, 98 insertions(+), 61 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4a4985c470..827ac564f8 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3898,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() @@ -4146,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() @@ -4898,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() @@ -4906,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() @@ -4917,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() @@ -5110,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() @@ -5122,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() @@ -5157,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(); @@ -5165,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(); @@ -5254,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() @@ -5367,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(); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index ae176dccaa..a63ff7b0bd 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/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 220278fd1d..9530b2c2e2 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -103,7 +103,10 @@ pub fn init(cx: &mut MutableAppContext) { } pub enum Event { - OpenedEntry(ProjectEntryId), + OpenedEntry { + entry_id: ProjectEntryId, + focus_opened_item: bool, + }, } impl ProjectPanel { @@ -157,19 +160,29 @@ impl ProjectPanel { 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.clone(); + 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 { + cx.focus(&project_panel); + } + } } } } @@ -198,7 +211,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); } } @@ -342,7 +358,10 @@ impl ProjectPanel { } fn open_entry(&mut self, action: &Open, cx: &mut ViewContext) { - cx.emit(Event::OpenedEntry(action.entry_id)); + cx.emit(Event::OpenedEntry { + entry_id: action.entry_id, + focus_opened_item: action.change_focus, + }); } fn add_file(&mut self, _: &AddFile, cx: &mut ViewContext) { diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 400a8e467a..f99fceef3a 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/pane.rs b/crates/workspace/src/pane.rs index 94fe7c3a42..e963ca7e02 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, @@ -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/workspace.rs b/crates/workspace/src/workspace.rs index fe18fcb95a..499ae037c5 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, ) @@ -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(); @@ -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 20b5626834..77e400e02f 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::() From 954fabec426d7adea0cca53b9476eb6ed2d2addb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 May 2022 15:34:36 +0200 Subject: [PATCH 23/26] Don't hide sidebar when hitting `cmd-1` Co-Authored-By: Nathan Sobo --- crates/workspace/src/workspace.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 499ae037c5..b23710834d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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 { From 6021ab12c9b1b950dccc3a9ec1a89df08970cdde Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 May 2022 15:43:42 +0200 Subject: [PATCH 24/26] Clear project browser editor even if an operation fails Co-Authored-By: Nathan Sobo --- crates/project_panel/src/project_panel.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9530b2c2e2..7d459ad99a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -334,9 +334,14 @@ impl ProjectPanel { cx.notify(); Some(cx.spawn(|this, mut cx| async move { - let new_entry = edit_task.await?; + 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; From 2e9bdfbeac0d01ff9db465f6f8f34b4dd3e8ef79 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 May 2022 15:49:40 +0200 Subject: [PATCH 25/26] Improve delete prompt in project browser Co-Authored-By: Nathan Sobo --- crates/project_panel/src/project_panel.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7d459ad99a..b88e14f2de 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -464,7 +464,14 @@ impl ProjectPanel { fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { let Selection { entry_id, .. } = self.selection?; - let mut answer = cx.prompt(PromptLevel::Info, "Delete?", &["Delete", "Cancel"]); + 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(()); From 76ad563b45d6f0d931e6f4a41dd0ecee7c33de29 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 May 2022 15:52:46 +0200 Subject: [PATCH 26/26] Fix memory leak of `ProjectPanel` Co-Authored-By: Nathan Sobo --- crates/project_panel/src/project_panel.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b88e14f2de..61c97f281d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -161,7 +161,7 @@ impl ProjectPanel { this }); cx.subscribe(&project_panel, { - let project_panel = project_panel.clone(); + let project_panel = project_panel.downgrade(); move |workspace, _, event, cx| match event { &Event::OpenedEntry { entry_id, @@ -180,7 +180,9 @@ impl ProjectPanel { ) .detach_and_log_err(cx); if !focus_opened_item { - cx.focus(&project_panel); + if let Some(project_panel) = project_panel.upgrade(cx) { + cx.focus(&project_panel); + } } } }