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 4b53480c7956768c91cc36108d00c7eff2e43309..0dde59872776c605827b7acb57fd1d2d7d438524 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 54e4e6eac841ba2dac9b98d6644dffa3b4a50ed2..495371914a9a42bcf69f828bc92d0383c00a49bd 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 91b0e57ac2450859809467921baab4c0a4ac93d3..528e3ca91ff4aa565a06c734cf09147af312e6ec 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 b66a8fe9ec7fc33f9aa5dc7d40bc20317c5b3918..240b2627c84c194b4e86673f799c380822dff665 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 92d340a9b44a179742dc0972528a633bed58dfe2..52c3e753c216fea8a5be76bbe94659bdb3bf4fd7 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 ee5acf2712fed50742083c72340cf3769f360e96..f11c1515a36987e8fd9a6eb9b067381718f9fd86 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 7ae3c8870313fd5a192392199aea94f100ed8814..023dc64ddd414513e9cc596a7e9d2a8b9c258838 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 69eb194e3ce57d4a23c0bb60e491607b65d3cc04..38679f2a9609ddcbb9883603a81108748ee938a2 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 6f3cc17207a18980a72d34f422ae084739680202..a98445d0b0f7f50198909d4636bc9d8c6d4e61c7 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 c0959a0e5e89b03fb972ecb0b1c431012f66878d..c2b1fc26a06a336d0df0e0496e12800f6fe7e26b 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 068fd547366bfac4953f8d8cbac2e43d280473d2..55a1e1b81c408cd9370ef1c44836e3629e885014 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 f47636c0932a1376422865ebec7ddcc0349a1228..440a5d53469f119c007cdf32e8b6a148121daaa5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9354,19 +9354,10 @@ mod tests { let fs = FakeFs::new(cx.background().clone()); fs.insert_file("/file.rs", Default::default()).await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/file.rs"], cx).await; project.update(cx, |project, _| project.languages().add(Arc::new(language))); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/file.rs", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .await .unwrap(); @@ -9485,19 +9476,10 @@ mod tests { let fs = FakeFs::new(cx.background().clone()); fs.insert_file("/file.rs", text).await; - let project = Project::test(fs, cx); + let project = Project::test(fs, ["/file.rs"], cx).await; project.update(cx, |project, _| project.languages().add(Arc::new(language))); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/file.rs", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .await .unwrap(); let mut fake_server = fake_servers.next().await.unwrap(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a9f24f2b6155cde6e3d8ca9e5b93de93165f1a73..f8e25ff2ccae63e4b8be6989d9bc3e9e9da503d9 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 (tree, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree(&root_link_path, true, cx) - }) - .await - .unwrap(); + let project = Project::test(Arc::new(RealFs), [root_link_path], cx).await; - 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 dir = temp_tree(json!({ - "file1": "abc", - "file2": "def", - "file3": "ghi", - })); - - let project = Project::test(Arc::new(RealFs), cx); - let (worktree, _) = project - .update(cx, |p, cx| { - p.find_or_create_local_worktree(dir.path(), true, cx) - }) - .await - .unwrap(); - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "file1": "abc", + "file2": "def", + "file3": "ghi", + }), + ) + .await; - worktree.flush_fs_events(&cx).await; - worktree - .read_with(cx, |t, _| t.as_local().unwrap().scan_complete()) - .await; + let project = Project::test(fs.clone(), ["/dir"], cx).await; let buffer1 = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) + .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) .await .unwrap(); let events = Rc::new(RefCell::new(Vec::new())); @@ -7148,7 +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 a98445d0b0f7f50198909d4636bc9d8c6d4e61c7..ff14cffe7cc824648c6792d94cefda987bad65f6 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 a35d44a81049c175158e7d6287c501dddcf0f517..4f083888d9d04d55e8c35f8ecea3afe1446c9491 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -297,23 +297,12 @@ mod tests { let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({ "test.rs": "" })).await; - let project = Project::test(fs.clone(), cx); - project.update(cx, |project, _| { - project.languages().add(Arc::new(language)); - }); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |tree, _| tree.id()); + let project = Project::test(fs.clone(), ["/dir"], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); let _buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "test.rs"), cx) + project.open_local_buffer("/dir/test.rs", cx) }) .await .unwrap(); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 39870a31d5c0f3722045e736638ea7c2f964e5fc..d3b3520a53b36c324714718145c138e9aa6e006d 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 | 535 +++++++++++++++++++--- 4 files changed, 464 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23a87887a5e7f2c179881355cc854a093985eba7..d74f09da1d385eb061912797e3b9056ddd6631a9 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 19b490b0e294a302f8122b43444d819561830422..b7a075395b66184c8483659339fdb68337671f99 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 4bd4f04570a1352e18b93be00437f7396370681a..caedaef8b1a8e0d54bdd141a7681ece5880533a8 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 ff14cffe7cc824648c6792d94cefda987bad65f6..a28fc5e46e83acc538375f53948fe83fa110a3b3 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 - } + }, ] ); + } - 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; - } + #[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": "" } } - panic!("no worktree for path {:?}", path); + }), + ) + .await; + fs.insert_tree( + "/root2", + json!({ + "d": { + "9": "" + }, + "e": {} + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1", "/root2"], cx).await; + let params = cx.update(WorkspaceParams::test); + let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); + let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx)); + + select_path(&panel, "root1", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1 <== selected", + " > a", + " > b", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + // Add a file with the root folder selected. The filename editor is placed + // before the first file in the root folder. + panel.update(cx, |panel, cx| panel.add_file(&AddFile, cx)); + assert!(panel.read_with(cx, |panel, cx| panel.filename_editor.is_focused(cx))); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1 <== selected", + " > a", + " > b", + " > C", + " [NEW FILE EDITOR]", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + 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(); - panel.update(cx, |panel, cx| { - panel.for_each_visible_entry(range, cx, |project_entry, details, _| { + 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 b7a075395b66184c8483659339fdb68337671f99..812783aaaf92e41fb60eeb384d17d87e729daa4f 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 caedaef8b1a8e0d54bdd141a7681ece5880533a8..4b78f2a1fae0ca9de788b6045d7b5ceab5af301a 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 a28fc5e46e83acc538375f53948fe83fa110a3b3..37f14d7962617f1ea96b13e9d07c5ba2b47c8bf7 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 1378022bcf4b46f698e373df3df3c8544d4bef63..b7d390062fc773a1eb7151c0b3fc2c97d51d96e4 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 0dde59872776c605827b7acb57fd1d2d7d438524..3b19c2e63eef855981632972fe933324dd677902 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 495371914a9a42bcf69f828bc92d0383c00a49bd..2e33fb774fe95b06c4da7d8d7d6209c20673b592 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 528e3ca91ff4aa565a06c734cf09147af312e6ec..ba9b7189d35fada8abcc2b42bace6c9f5a81fd19 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 240b2627c84c194b4e86673f799c380822dff665..7cbd315c8a41ff3572cb414727618607ae3e8875 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 52c3e753c216fea8a5be76bbe94659bdb3bf4fd7..8672518b4cf3ef8a93cc9ad4793a6ba0106c2faa 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 f11c1515a36987e8fd9a6eb9b067381718f9fd86..66b43e613dc725687d3f34c3ab427d7a1311ae61 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 023dc64ddd414513e9cc596a7e9d2a8b9c258838..66f5182172e01e14902e28566600bbd7084de1de 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 38679f2a9609ddcbb9883603a81108748ee938a2..34a33897288143e9e5c8492d5c753232ac1422b9 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 28ef9e5af04f2f7c5be3cb2338edf27c87facac1..f4453774f7b9ff784d27154bd2db51bced27d888 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 f8e25ff2ccae63e4b8be6989d9bc3e9e9da503d9..891f963e1deedfa5674e0c710b2e17e4054c6eca 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 812783aaaf92e41fb60eeb384d17d87e729daa4f..7651e332daaf47fa717595d1e68d148429e8b465 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 37f14d7962617f1ea96b13e9d07c5ba2b47c8bf7..d038ac941fe8611ffc05ebf3b2fa540bc18fe76b 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 c2b1fc26a06a336d0df0e0496e12800f6fe7e26b..d64c093144d752c1cb6a88105c3bd7cfea18be7c 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 55a1e1b81c408cd9370ef1c44836e3629e885014..bacc3590e552c5900e113825229574af6c82e98d 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 7651e332daaf47fa717595d1e68d148429e8b465..8116a8b973ca08ba17ced77e390defcdf7bd3a4f 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 ec7925685d69173609d7344edad9521d363183ce..bc815f5be1becf3243263c2df7faf71c0930c6b6 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 d038ac941fe8611ffc05ebf3b2fa540bc18fe76b..001667d31184b7a89fbd6b44e081125aad77b602 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 891f963e1deedfa5674e0c710b2e17e4054c6eca..97697c26f2b9b688efa7aa9e4b37008bb0fe6f45 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 8116a8b973ca08ba17ced77e390defcdf7bd3a4f..a8e630c1a3fbb2b78c139245a86062bd149d2b4f 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_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(&snapshot.abs_path).await { + let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&root_abs_path).await { abs_path } else { return false; }; - let root_char_bag = snapshot.root_char_bag; - let next_entry_id = snapshot.next_entry_id.clone(); - - events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); - events.dedup_by(|a, b| a.path.starts_with(&b.path)); + let metadata = futures::future::join_all( + events + .iter() + .map(|event| self.fs.metadata(&event.path)) + .collect::>(), + ) + .await; - for event in &events { - match event.path.strip_prefix(&root_abs_path) { - Ok(path) => snapshot.remove_path(&path), - Err(_) => { - log::error!( - "unexpected event {:?} for root path {:?}", - event.path, - root_abs_path - ); - continue; + // Hold the snapshot lock while clearing and re-inserting the root entries + // for each event. This way, the snapshot is not observable to the foreground + // thread while this operation is in-progress. + let (scan_queue_tx, scan_queue_rx) = channel::unbounded(); + { + let mut snapshot = self.snapshot.lock(); + for event in &events { + if let Ok(path) = event.path.strip_prefix(&root_abs_path) { + snapshot.remove_path(&path); } } - } - let (scan_queue_tx, scan_queue_rx) = channel::unbounded(); - for event in events { - let path: Arc = match event.path.strip_prefix(&root_abs_path) { - Ok(path) => Arc::from(path.to_path_buf()), - Err(_) => { - log::error!( - "unexpected event {:?} for root path {:?}", - event.path, - root_abs_path - ); - continue; - } - }; + for (event, metadata) in events.into_iter().zip(metadata.into_iter()) { + let path: Arc = match event.path.strip_prefix(&root_abs_path) { + Ok(path) => Arc::from(path.to_path_buf()), + Err(_) => { + log::error!( + "unexpected event {:?} for root path {:?}", + event.path, + root_abs_path + ); + continue; + } + }; - match self.fs.metadata(&event.path).await { - Ok(Some(metadata)) => { - let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir); - let mut fs_entry = Entry::new( - path.clone(), - &metadata, - snapshot.next_entry_id.as_ref(), - snapshot.root_char_bag, - ); - fs_entry.is_ignored = ignore_stack.is_all(); - snapshot.insert_entry(fs_entry, self.fs.as_ref()); - if metadata.is_dir { - scan_queue_tx - .send(ScanJob { - abs_path: event.path, - path, - ignore_stack, - scan_queue: scan_queue_tx.clone(), - }) - .await - .unwrap(); + match metadata { + Ok(Some(metadata)) => { + let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir); + let mut fs_entry = Entry::new( + path.clone(), + &metadata, + snapshot.next_entry_id.as_ref(), + snapshot.root_char_bag, + ); + fs_entry.is_ignored = ignore_stack.is_all(); + snapshot.insert_entry(fs_entry, self.fs.as_ref()); + if metadata.is_dir { + self.executor + .block(scan_queue_tx.send(ScanJob { + abs_path: event.path, + path, + ignore_stack, + scan_queue: scan_queue_tx.clone(), + })) + .unwrap(); + } + } + Ok(None) => {} + Err(err) => { + // TODO - create a special 'error' entry in the entries tree to mark this + log::error!("error reading file on event {:?}", err); } - } - Ok(None) => {} - Err(err) => { - // TODO - create a special 'error' entry in the entries tree to mark this - log::error!("error reading file on event {:?}", err); } } + drop(scan_queue_tx); } - *self.snapshot.lock() = snapshot; - // Scan any directories that were created as part of this event batch. - drop(scan_queue_tx); self.executor .scoped(|scope| { for _ in 0..self.executor.num_cpus() { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 001667d31184b7a89fbd6b44e081125aad77b602..90d0e30b6535121c8e110a540caf01e72767e734 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 | 135 +++++++++++------- crates/rpc/src/proto.rs | 8 ++ 9 files changed, 349 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d74f09da1d385eb061912797e3b9056ddd6631a9..77dd2908ba8ba62c2fd4a8f0d43a87940e6b6397 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 1813d8f52d349fd57046fa6789b9730f549f269c..faf1da297dfa789ff3ab0be77e1e41ac55e3d618 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 2c80e01d6df6619395f587926cecd6c334eb6569..3126e18c7aef6caf6a51e9b97e263b23199e5f78 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -360,6 +360,14 @@ impl Deterministic { self.state.lock().now = new_now; } + + pub fn forbid_parking(&self) { + use rand::prelude::*; + + let mut state = self.state.lock(); + state.forbid_parking = true; + state.rng = StdRng::seed_from_u64(state.seed); + } } impl Drop for Timer { @@ -507,14 +515,8 @@ impl Foreground { #[cfg(any(test, feature = "test-support"))] pub fn forbid_parking(&self) { - use rand::prelude::*; - match self { - Self::Deterministic { executor, .. } => { - let mut state = executor.state.lock(); - state.forbid_parking = true; - state.rng = StdRng::seed_from_u64(state.seed); - } + Self::Deterministic { executor, .. } => executor.forbid_parking(), _ => panic!("this method can only be called on a deterministic executor"), } } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 728dae312852480908315becf79e6c7ded7b9e99..eaae45bcc62d31f68adde4a8492c9d4f2e9a9f7b 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -29,7 +29,7 @@ settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } aho-corasick = "0.7" -anyhow = "1.0.38" +anyhow = "1.0.57" async-trait = "0.1" futures = "0.3" ignore = "0.4" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 97697c26f2b9b688efa7aa9e4b37008bb0fe6f45..307ef16d3af8de4fd7a4bc5be7fa5965499b99bc 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 a8e630c1a3fbb2b78c139245a86062bd149d2b4f..d4d6160a4d05428b6c0bdceba17ab86a39650dc6 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 90d0e30b6535121c8e110a540caf01e72767e734..0ee26d9c39116711c614ef17558627932a7e6e2a 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 bf18db9e2ba7d5efce655617e68aa018364af99a..3fc350e9b60371650d6dd9352100a8b669e21656 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -36,57 +36,63 @@ message Envelope { RegisterWorktree register_worktree = 28; UnregisterWorktree unregister_worktree = 29; UpdateWorktree update_worktree = 31; - UpdateDiagnosticSummary update_diagnostic_summary = 32; - StartLanguageServer start_language_server = 33; - UpdateLanguageServer update_language_server = 34; - - OpenBufferById open_buffer_by_id = 35; - OpenBufferByPath open_buffer_by_path = 36; - OpenBufferResponse open_buffer_response = 37; - UpdateBuffer update_buffer = 38; - UpdateBufferFile update_buffer_file = 39; - SaveBuffer save_buffer = 40; - BufferSaved buffer_saved = 41; - BufferReloaded buffer_reloaded = 42; - ReloadBuffers reload_buffers = 43; - ReloadBuffersResponse reload_buffers_response = 44; - FormatBuffers format_buffers = 45; - FormatBuffersResponse format_buffers_response = 46; - GetCompletions get_completions = 47; - GetCompletionsResponse get_completions_response = 48; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 49; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 50; - GetCodeActions get_code_actions = 51; - GetCodeActionsResponse get_code_actions_response = 52; - ApplyCodeAction apply_code_action = 53; - ApplyCodeActionResponse apply_code_action_response = 54; - PrepareRename prepare_rename = 55; - PrepareRenameResponse prepare_rename_response = 56; - PerformRename perform_rename = 57; - PerformRenameResponse perform_rename_response = 58; - SearchProject search_project = 59; - SearchProjectResponse search_project_response = 60; - - GetChannels get_channels = 61; - GetChannelsResponse get_channels_response = 62; - JoinChannel join_channel = 63; - JoinChannelResponse join_channel_response = 64; - LeaveChannel leave_channel = 65; - SendChannelMessage send_channel_message = 66; - SendChannelMessageResponse send_channel_message_response = 67; - ChannelMessageSent channel_message_sent = 68; - GetChannelMessages get_channel_messages = 69; - GetChannelMessagesResponse get_channel_messages_response = 70; - - UpdateContacts update_contacts = 71; - - GetUsers get_users = 72; - GetUsersResponse get_users_response = 73; - - Follow follow = 74; - FollowResponse follow_response = 75; - UpdateFollowers update_followers = 76; - Unfollow unfollow = 77; + + CreateProjectEntry create_project_entry = 32; + CreateProjectEntryResponse create_project_entry_response = 33; + RenameProjectEntry rename_project_entry = 34; + DeleteProjectEntry delete_project_entry = 35; + + UpdateDiagnosticSummary update_diagnostic_summary = 36; + StartLanguageServer start_language_server = 37; + UpdateLanguageServer update_language_server = 38; + + OpenBufferById open_buffer_by_id = 39; + OpenBufferByPath open_buffer_by_path = 40; + OpenBufferResponse open_buffer_response = 41; + UpdateBuffer update_buffer = 42; + UpdateBufferFile update_buffer_file = 43; + SaveBuffer save_buffer = 44; + BufferSaved buffer_saved = 45; + BufferReloaded buffer_reloaded = 46; + ReloadBuffers reload_buffers = 47; + ReloadBuffersResponse reload_buffers_response = 48; + FormatBuffers format_buffers = 49; + FormatBuffersResponse format_buffers_response = 50; + GetCompletions get_completions = 51; + GetCompletionsResponse get_completions_response = 52; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 53; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 54; + GetCodeActions get_code_actions = 55; + GetCodeActionsResponse get_code_actions_response = 56; + ApplyCodeAction apply_code_action = 57; + ApplyCodeActionResponse apply_code_action_response = 58; + PrepareRename prepare_rename = 59; + PrepareRenameResponse prepare_rename_response = 60; + PerformRename perform_rename = 61; + PerformRenameResponse perform_rename_response = 62; + SearchProject search_project = 63; + SearchProjectResponse search_project_response = 64; + + GetChannels get_channels = 65; + GetChannelsResponse get_channels_response = 66; + JoinChannel join_channel = 67; + JoinChannelResponse join_channel_response = 68; + LeaveChannel leave_channel = 69; + SendChannelMessage send_channel_message = 70; + SendChannelMessageResponse send_channel_message_response = 71; + ChannelMessageSent channel_message_sent = 72; + GetChannelMessages get_channel_messages = 73; + GetChannelMessagesResponse get_channel_messages_response = 74; + + UpdateContacts update_contacts = 75; + + GetUsers get_users = 76; + GetUsersResponse get_users_response = 77; + + Follow follow = 78; + FollowResponse follow_response = 79; + UpdateFollowers update_followers = 80; + Unfollow unfollow = 81; } } @@ -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 98fc493774041ddc84fe0a054ff713651c3db887..a1c28075f5c922fdad0a2ce3f888a859fb47b770 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 307ef16d3af8de4fd7a4bc5be7fa5965499b99bc..ea95a6e91d15828a174549e02e9350067ffa50f5 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 d4d6160a4d05428b6c0bdceba17ab86a39650dc6..65d5871bb6231b38f54c7b69bf49f5ba991f0ae5 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 0ee26d9c39116711c614ef17558627932a7e6e2a..dba6ce10375a871f5f9d32b1d010bd0c391fde08 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 faf1da297dfa789ff3ab0be77e1e41ac55e3d618..cfea7cbc162518a8e35931d71ac4885561935ec0 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 ea95a6e91d15828a174549e02e9350067ffa50f5..7d1aacdde97352a60a355584e7ca39ec54e0436a 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 65d5871bb6231b38f54c7b69bf49f5ba991f0ae5..7b3c7009111b127f9addaeaa84b554fbcfe24286 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 dba6ce10375a871f5f9d32b1d010bd0c391fde08..5c96a254e58bb05e02774ceb39fecfd8103c8cf7 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 3fc350e9b60371650d6dd9352100a8b669e21656..ffa2443537fcb4d39c96bfa79f017cd96833f4f2 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 a1c28075f5c922fdad0a2ce3f888a859fb47b770..b2bcaa7d5fec29c1cc49e14cc77ad94a016fa03c 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 c77b10e1bd110f15bc9e1def5f63b204c968a85a..d524735bacb2d2c8cf8157474398b19eb4c7c3ff 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 e0f6a0133c88a086248288784f4ff530ae7e9066..ffddcb9cd3ce1ac232b8eacd6b4ebeb08580c1b4 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -5,4 +5,4 @@ pub mod proto; pub use conn::Connection; pub use peer::*; -pub const PROTOCOL_VERSION: u32 = 15; +pub const PROTOCOL_VERSION: u32 = 16; 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 5c96a254e58bb05e02774ceb39fecfd8103c8cf7..b76166886ad29c4c92c6dd16cba81b451307dba3 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 cfea7cbc162518a8e35931d71ac4885561935ec0..8473e28c660aea5ea46cfccd07d71ff5f9cdcda3 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 7d1aacdde97352a60a355584e7ca39ec54e0436a..26a9ed14d7cc4e49bfce4c125671e25ef61cebe3 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 7b3c7009111b127f9addaeaa84b554fbcfe24286..2efeea1645aec037e0973aedf7965963920a7455 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 b2bcaa7d5fec29c1cc49e14cc77ad94a016fa03c..59053997d3632f3c0882a95783a497debf52f4cb 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 d524735bacb2d2c8cf8157474398b19eb4c7c3ff..193786112b46f5dec84c0343ca8e3c9d5c24926a 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -483,17 +483,20 @@ impl PartialEq for SumTree { impl Eq for SumTree {} impl SumTree { - pub fn insert_or_replace(&mut self, item: T, cx: &::Context) -> bool { - let mut replaced = false; + pub fn insert_or_replace( + &mut self, + item: T, + cx: &::Context, + ) -> Option { + let mut replaced = None; *self = { let mut cursor = self.cursor::(); let mut new_tree = cursor.slice(&item.key(), Bias::Left, cx); - if cursor - .item() - .map_or(false, |cursor_item| cursor_item.key() == item.key()) - { - cursor.next(cx); - replaced = true; + if let Some(cursor_item) = cursor.item() { + if cursor_item.key() == item.key() { + replaced = Some(cursor_item.clone()); + cursor.next(cx); + } } new_tree.push(item, cx); new_tree.push_tree(cursor.suffix(cx), cx); 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 8473e28c660aea5ea46cfccd07d71ff5f9cdcda3..a429a16a47ce8efc7c8170d89e4672b51241c4f6 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 26a9ed14d7cc4e49bfce4c125671e25ef61cebe3..d0891cadb17d728be90e1cd6e358cacae925a2dd 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 2efeea1645aec037e0973aedf7965963920a7455..db4769b7aa3c72d47fa7780a16dce71c18e4ea1e 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 b76166886ad29c4c92c6dd16cba81b451307dba3..18ff22cd95664254ccc02d4364f275a04ba63f11 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 a429a16a47ce8efc7c8170d89e4672b51241c4f6..624eba59a3bb3985fdfd01aa3beecef9cd901491 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 d0891cadb17d728be90e1cd6e358cacae925a2dd..5590c617622720dad21ff8fe26dcb1230ab7aa26 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 77dd2908ba8ba62c2fd4a8f0d43a87940e6b6397..6d7ce98341f0dab435d1148710f2134ebae582d2 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 b7d390062fc773a1eb7151c0b3fc2c97d51d96e4..dd83b91ed06f95361349d340e2c708f8145acc46 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 624eba59a3bb3985fdfd01aa3beecef9cd901491..2367532d28a314bafab9bb284447493025f0401c 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 5590c617622720dad21ff8fe26dcb1230ab7aa26..7af1199ec107b9c10a1f12af592e929dea964ac8 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 db4769b7aa3c72d47fa7780a16dce71c18e4ea1e..2a1808457cc61b35691a44c98973c99db00361f3 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 4b78f2a1fae0ca9de788b6045d7b5ceab5af301a..e431db45ddd392f28964e073576d8df1845777c3 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 18ff22cd95664254ccc02d4364f275a04ba63f11..6bb4f640ee8a92b3b906f0bea121586d5ee266b0 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 ffa2443537fcb4d39c96bfa79f017cd96833f4f2..8a30278920ef5296181029e7b53fab4e1bc41a8d 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 59053997d3632f3c0882a95783a497debf52f4cb..428eb13a42a9920ee309e90932cd72350033038c 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 bc815f5be1becf3243263c2df7faf71c0930c6b6..912dc65afeae289e4283a44f9d0a6d2bdc281956 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 2367532d28a314bafab9bb284447493025f0401c..d17473e1398013b459cd94c8874ea375ec2c6d2b 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 3be072c5e2e67b84b7cf5f70c7367f2e4b3be4d6..4737dd2c804ded463841948413d404d09d0858c0 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -46,6 +46,7 @@ pub struct ProjectShare { pub struct WorktreeShare { pub entries: HashMap, pub diagnostic_summaries: BTreeMap, + pub scan_id: u64, } #[derive(Default)] @@ -561,6 +562,7 @@ impl Store { worktree_id: u64, removed_entries: &[u64], updated_entries: &[proto::Entry], + scan_id: u64, ) -> Result> { let project = self.write_project(project_id, connection_id)?; let worktree = project @@ -574,6 +576,7 @@ impl Store { for entry in updated_entries { worktree.entries.insert(entry.id, entry.clone()); } + worktree.scan_id = scan_id; let connection_ids = project.connection_ids(); Ok(connection_ids) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7af1199ec107b9c10a1f12af592e929dea964ac8..1fcd89fcde90bee62b0a3831b4e2df3d491ed956 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 2a1808457cc61b35691a44c98973c99db00361f3..bab41bbe278e21330b1f695a09feb9375efc105f 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 8a30278920ef5296181029e7b53fab4e1bc41a8d..fa0b587df486957d65b87c97caaf5b9a3eb5ed8f 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 428eb13a42a9920ee309e90932cd72350033038c..c505869c554744e1c89911b0a9c5f556ac3dd8b0 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 d17473e1398013b459cd94c8874ea375ec2c6d2b..4a4985c47062bb29b20210bceddf3843f8232e39 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 f722167b6f3696deaabcd671ea64d1c0fb908efe..499b3ed99d579af99ec02dddadad9a6aa77dd0d6 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -270,7 +270,7 @@ impl View for AutoUpdateIndicator { ) .boxed() }) - .on_click(|cx| cx.dispatch_action(DismissErrorMessage)) + .on_click(|_, cx| cx.dispatch_action(DismissErrorMessage)) .boxed() } AutoUpdateStatus::Idle => Empty::new().boxed(), diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 415ff6187ec72cca09c04cff901f34c9edadab27..bb835c66401d595607546fbca5453d85461c2164 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -320,7 +320,7 @@ impl ChatPanel { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |cx| { + .on_click(move |_, cx| { let rpc = rpc.clone(); let this = this.clone(); cx.spawn(|mut cx| async move { diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 45b5f69b5e2efea846b6ae49716777ea9ae0b7a1..171b4194960fc54f7c61b5d50e5e46d2a4b69c81 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -204,7 +204,7 @@ impl ContactsPanel { } else { CursorStyle::Arrow }) - .on_click(move |cx| { + .on_click(move |_, cx| { if !is_host && !is_guest { cx.dispatch_global_action(JoinProject { project_id, diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 39b5437a6a49360250e3ce28d8d4ae1885cfde67..ef99cbf5a87868d54c3c334c2957f8f6f2022d58 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -161,7 +161,7 @@ impl View for DiagnosticIndicator { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|cx| cx.dispatch_action(crate::Deploy)) + .on_click(|_, cx| cx.dispatch_action(crate::Deploy)) .aligned() .boxed(), ); @@ -194,7 +194,7 @@ impl View for DiagnosticIndicator { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(|cx| cx.dispatch_action(GoToNextDiagnostic)) + .on_click(|_, cx| cx.dispatch_action(GoToNextDiagnostic)) .boxed(), ); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f4453774f7b9ff784d27154bd2db51bced27d888..a04d27a8bb669cc5427562069a9bed444421f591 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 12166d45b541b3d9316c372a464b2e121775b4ca..1ee7c6cbb5e57bf2108d36f48424fd3c9db7e058 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -15,7 +15,7 @@ pub struct MouseEventHandler { child: ElementBox, cursor_style: Option, mouse_down_handler: Option>, - click_handler: Option>, + click_handler: Option>, drag_handler: Option>, padding: Padding, } @@ -57,7 +57,7 @@ impl MouseEventHandler { self } - pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self { + pub fn on_click(mut self, handler: impl FnMut(usize, &mut EventContext) + 'static) -> Self { self.click_handler = Some(Box::new(handler)); self } @@ -151,14 +151,18 @@ impl Element for MouseEventHandler { handled_in_child } } - Event::LeftMouseUp { position, .. } => { + Event::LeftMouseUp { + position, + click_count, + .. + } => { state.prev_drag_position = None; if !handled_in_child && state.clicked { state.clicked = false; cx.notify(); if let Some(handler) = click_handler { if hit_bounds.contains_point(*position) { - handler(cx); + handler(*click_count, cx); } } true diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index fe353fed4c9be36df5b74aa474be07d5123f3ad1..b32ab952c79dfbef1b1a539bec117463954c25bf 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -28,6 +28,7 @@ pub enum Event { }, LeftMouseUp { position: Vector2F, + click_count: usize, }, LeftMouseDragged { position: Vector2F, @@ -68,7 +69,7 @@ impl Event { Event::KeyDown { .. } => None, Event::ScrollWheel { position, .. } | Event::LeftMouseDown { position, .. } - | Event::LeftMouseUp { position } + | Event::LeftMouseUp { position, .. } | Event::LeftMouseDragged { position } | Event::RightMouseDown { position, .. } | Event::RightMouseUp { position } diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 7170bd2fd598d81301a3d10342f93c24040301d9..651805370c0be39bc17d5a0dde379f7c9e825a07 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -129,6 +129,7 @@ impl Event { native_event.locationInWindow().x as f32, window_height - native_event.locationInWindow().y as f32, ), + click_count: native_event.clickCount() as usize, }), NSEventType::NSRightMouseDown => { let modifiers = native_event.modifierFlags(); diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index 10cd0cd5a2934912665917e85237d2bb67b28575..d5d2105c3f34e6b74126080771118f07df8903af 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -119,7 +119,7 @@ impl View for Select { .with_style(style.header) .boxed() }) - .on_click(move |cx| cx.dispatch_action(ToggleSelect)) + .on_click(move |_, cx| cx.dispatch_action(ToggleSelect)) .boxed(), ); if self.is_open { @@ -153,7 +153,7 @@ impl View for Select { ) }, ) - .on_click(move |cx| cx.dispatch_action(SelectItem(ix))) + .on_click(move |_, cx| cx.dispatch_action(SelectItem(ix))) .boxed() })) }, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 6bb4f640ee8a92b3b906f0bea121586d5ee266b0..220278fd1d627426c1e96fda6a205e555b680d3f 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 d1f17608f28c98f4e805e865890211104bd8449a..549edf89e71cdc356f20b3911b39922323cbef19 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -292,7 +292,7 @@ impl BufferSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option))) + .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(search_option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -316,7 +316,7 @@ impl BufferSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |cx| match direction { + .on_click(move |_, cx| match direction { Direction::Prev => cx.dispatch_action(SelectPrevMatch), Direction::Next => cx.dispatch_action(SelectNextMatch), }) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d3b3520a53b36c324714718145c138e9aa6e006d..cbd373c468f700533d24065568f41e54960a4141 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -666,7 +666,7 @@ impl ProjectSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |cx| match direction { + .on_click(move |_, cx| match direction { Direction::Prev => cx.dispatch_action(SelectPrevMatch), Direction::Next => cx.dispatch_action(SelectNextMatch), }) @@ -693,7 +693,7 @@ impl ProjectSearchBar { .with_style(style.container) .boxed() }) - .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) + .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index ddc6d893086dfe71d73fa57f887800fa64445749..f58e0b973e05e27db44359b95c11524010e14ea2 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -168,7 +168,7 @@ impl View for LspStatus { self.failed.join(", "), if self.failed.len() > 1 { "s" } else { "" } ); - handler = Some(|cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage)); + handler = Some(|_, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage)); } else { return Empty::new().boxed(); } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a992897c1182c9e03a5ee515d6a71fd7cf2bc8d7..94fe7c3a42f67db6b517d8cd75cd0313107c15d4 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 2f13469cec47ca08b7904c1d3885523932cd277a..bc7314e73286b48ed7f2ca95bb56cd7e93f71de8 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -203,7 +203,7 @@ impl View for SidebarButtons { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |cx| { + .on_click(move |_, cx| { cx.dispatch_action(ToggleSidebarItem { side, item_index: ix, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d072c515503edb02ad6f52d9c55c87ca850d02d8..fe18fcb95a72d0c8ba05fbf3e0ae5fd48cf5796f 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 4a4985c47062bb29b20210bceddf3843f8232e39..827ac564f827746700ea6456f35bd54289e64fe2 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 ae176dccaa9c8ec7ba5dfbdafecebd6b1226a7d4..a63ff7b0bd18c6270963bbaf759940b1042cbdde 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -102,7 +102,7 @@ impl FileFinder { match event { Event::Selected(project_path) => { workspace - .open_path(project_path.clone(), cx) + .open_path(project_path.clone(), true, cx) .detach_and_log_err(cx); workspace.dismiss_modal(cx); } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 220278fd1d627426c1e96fda6a205e555b680d3f..9530b2c2e2284e41101a7ca7287eb371ef9e5bc4 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 400a8e467a881c78c427c82f4e1e7414cf0d2dba..f99fceef3a504228c63fba785b6125013e280b79 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -50,7 +50,7 @@ impl<'a> VimTestContext<'a> { let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let item = workspace - .update(cx, |workspace, cx| workspace.open_path(file, cx)) + .update(cx, |workspace, cx| workspace.open_path(file, true, cx)) .await .expect("Could not open test file"); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 94fe7c3a42f67db6b517d8cd75cd0313107c15d4..e963ca7e02ab4d1f2d8179fcb37658f371926f8f 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -59,7 +59,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { - pane.activate_item(action.0, true, cx); + pane.activate_item(action.0, true, true, cx); }); cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { pane.activate_prev_item(cx); @@ -213,7 +213,7 @@ impl Pane { { let prev_active_item_index = pane.active_item_index; pane.nav_history.borrow_mut().set_mode(mode); - pane.activate_item(index, true, cx); + pane.activate_item(index, true, true, cx); pane.nav_history .borrow_mut() .set_mode(NavigationMode::Normal); @@ -257,6 +257,7 @@ impl Pane { workspace, pane.clone(), project_entry_id, + true, cx, build_item, ) @@ -287,6 +288,7 @@ impl Pane { workspace: &mut Workspace, pane: ViewHandle, project_entry_id: ProjectEntryId, + focus_item: bool, cx: &mut ViewContext, build_item: impl FnOnce(&mut MutableAppContext) -> Box, ) -> Box { @@ -294,7 +296,7 @@ impl Pane { for (ix, item) in pane.items.iter().enumerate() { if item.project_entry_id(cx) == Some(project_entry_id) { let item = item.boxed_clone(); - pane.activate_item(ix, true, cx); + pane.activate_item(ix, true, focus_item, cx); return Some(item); } } @@ -304,7 +306,7 @@ impl Pane { existing_item } else { let item = build_item(cx); - Self::add_item(workspace, pane, item.boxed_clone(), true, cx); + Self::add_item(workspace, pane, item.boxed_clone(), true, focus_item, cx); item } } @@ -313,12 +315,15 @@ impl Pane { workspace: &mut Workspace, pane: ViewHandle, item: Box, - local: bool, + activate_pane: bool, + focus_item: bool, cx: &mut ViewContext, ) { // Prevent adding the same item to the pane more than once. if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) { - pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx)); + pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, activate_pane, focus_item, cx) + }); return; } @@ -327,7 +332,7 @@ impl Pane { pane.update(cx, |pane, cx| { let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len()); pane.items.insert(item_idx, item); - pane.activate_item(item_idx, local, cx); + pane.activate_item(item_idx, activate_pane, focus_item, cx); cx.notify(); }); } @@ -378,7 +383,13 @@ impl Pane { self.items.iter().position(|i| i.id() == item.id()) } - pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext) { + pub fn activate_item( + &mut self, + index: usize, + activate_pane: bool, + focus_item: bool, + cx: &mut ViewContext, + ) { use NavigationMode::{GoingBack, GoingForward}; if index < self.items.len() { let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); @@ -387,11 +398,15 @@ impl Pane { && prev_active_item_ix < self.items.len()) { self.items[prev_active_item_ix].deactivated(cx); - cx.emit(Event::ActivateItem { local }); + cx.emit(Event::ActivateItem { + local: activate_pane, + }); } self.update_toolbar(cx); - if local { + if focus_item { self.focus_active_item(cx); + } + if activate_pane { self.activate(cx); } self.autoscroll = true; @@ -406,7 +421,7 @@ impl Pane { } else if self.items.len() > 0 { index = self.items.len() - 1; } - self.activate_item(index, true, cx); + self.activate_item(index, true, true, cx); } pub fn activate_next_item(&mut self, cx: &mut ViewContext) { @@ -416,7 +431,7 @@ impl Pane { } else { index = 0; } - self.activate_item(index, true, cx); + self.activate_item(index, true, true, cx); } fn close_active_item( @@ -498,7 +513,7 @@ impl Pane { if is_last_item_for_entry { if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) { let mut answer = pane.update(&mut cx, |pane, cx| { - pane.activate_item(item_to_close_ix, true, cx); + pane.activate_item(item_to_close_ix, true, true, cx); cx.prompt( PromptLevel::Warning, CONFLICT_MESSAGE, @@ -518,7 +533,7 @@ impl Pane { } else if cx.read(|cx| item.is_dirty(cx)) { if cx.read(|cx| item.can_save(cx)) { let mut answer = pane.update(&mut cx, |pane, cx| { - pane.activate_item(item_to_close_ix, true, cx); + pane.activate_item(item_to_close_ix, true, true, cx); cx.prompt( PromptLevel::Warning, DIRTY_MESSAGE, @@ -535,7 +550,7 @@ impl Pane { } } else if cx.read(|cx| item.can_save_as(cx)) { let mut answer = pane.update(&mut cx, |pane, cx| { - pane.activate_item(item_to_close_ix, true, cx); + pane.activate_item(item_to_close_ix, true, true, cx); cx.prompt( PromptLevel::Warning, DIRTY_MESSAGE, @@ -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 fe18fcb95a72d0c8ba05fbf3e0ae5fd48cf5796f..499ae037c563eb7cb8618ee9b62e1c79a7e49875 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 20b56268345dd77bf95ed6589fefc4d03b17a0fc..77e400e02f96d276002018c63048a1fd133d5b2d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -446,7 +446,7 @@ mod tests { // Open the first entry let entry_1 = workspace - .update(cx, |w, cx| w.open_path(file1.clone(), cx)) + .update(cx, |w, cx| w.open_path(file1.clone(), true, cx)) .await .unwrap(); cx.read(|cx| { @@ -460,7 +460,7 @@ mod tests { // Open the second entry workspace - .update(cx, |w, cx| w.open_path(file2.clone(), cx)) + .update(cx, |w, cx| w.open_path(file2.clone(), true, cx)) .await .unwrap(); cx.read(|cx| { @@ -474,7 +474,7 @@ mod tests { // Open the first entry again. The existing pane item is activated. let entry_1b = workspace - .update(cx, |w, cx| w.open_path(file1.clone(), cx)) + .update(cx, |w, cx| w.open_path(file1.clone(), true, cx)) .await .unwrap(); assert_eq!(entry_1.id(), entry_1b.id()); @@ -492,7 +492,7 @@ mod tests { workspace .update(cx, |w, cx| { w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx); - w.open_path(file2.clone(), cx) + w.open_path(file2.clone(), true, cx) }) .await .unwrap(); @@ -511,8 +511,8 @@ mod tests { // Open the third entry twice concurrently. Only one pane item is added. let (t1, t2) = workspace.update(cx, |w, cx| { ( - w.open_path(file3.clone(), cx), - w.open_path(file3.clone(), cx), + w.open_path(file3.clone(), true, cx), + w.open_path(file3.clone(), true, cx), ) }); t1.await.unwrap(); @@ -780,6 +780,7 @@ mod tests { worktree_id: worktree.read(cx).id(), path: Path::new("the-new-name.rs").into(), }, + true, cx, ) }) @@ -875,7 +876,7 @@ mod tests { let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); workspace - .update(cx, |w, cx| w.open_path(file1.clone(), cx)) + .update(cx, |w, cx| w.open_path(file1.clone(), true, cx)) .await .unwrap(); @@ -955,7 +956,7 @@ mod tests { let file3 = entries[2].clone(); let editor1 = workspace - .update(cx, |w, cx| w.open_path(file1.clone(), cx)) + .update(cx, |w, cx| w.open_path(file1.clone(), true, cx)) .await .unwrap() .downcast::() @@ -964,13 +965,13 @@ mod tests { editor.select_display_ranges(&[DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)], cx); }); let editor2 = workspace - .update(cx, |w, cx| w.open_path(file2.clone(), cx)) + .update(cx, |w, cx| w.open_path(file2.clone(), true, cx)) .await .unwrap() .downcast::() .unwrap(); let editor3 = workspace - .update(cx, |w, cx| w.open_path(file3.clone(), cx)) + .update(cx, |w, cx| w.open_path(file3.clone(), true, cx)) .await .unwrap() .downcast::() 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 499ae037c563eb7cb8618ee9b62e1c79a7e49875..b23710834dfd38f00edcaa3ae7cf454613b79d6b 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 9530b2c2e2284e41101a7ca7287eb371ef9e5bc4..7d459ad99a1ecc2634083c0fe7cead368e50af5b 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 7d459ad99a1ecc2634083c0fe7cead368e50af5b..b88e14f2dec9b9cc0b752ed2290cb87e03a0df56 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 b88e14f2dec9b9cc0b752ed2290cb87e03a0df56..61c97f281d327f01f1cad86bb81dac7bca92bc0f 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); + } } } }